Automated release: version and notes generated from pull requests merged since 2.7.0.
- feat(cut):
--vfr-guard average|sampled|offchooses what keeps a variable-frame-rate source from a stream copy.average(the default) is the guard every 2.x release had, unchanged: a source whose nominal and average frame rates differ (variable_frame_rate_suspected) re-encodes as--accurate(now reasonvfr); its packet timestamps are then sampled in up to five 6 s windows (no decoding) and reported invfr_check(measured:sampled_cfr/vfr/inconclusive,guard,windows,deltas), with a note naming--vfr-guard sampledwhen the sample is constant, as on an iPhone clip at 29.98 against a nominal 30.sampleddecides on the sample instead:sampled_cfrcopies,vfrre-encodes with reasonvfrandinconclusive(too few frames, or ffprobe failed) withvfr_inconclusive;offsamples and keeps the copy, with a note. Variable timing between the sampled windows is not seen. The sample runs under--dry-runand is skipped when nothing would copy (audio-only,--accurate,--codec).probe.py'svariable_frame_rate_suspectedkeeps its meaning. - fix(cut): a
--segmentssegment that runs past the end of the video no longer opens a hole at its join. A segment (not the last) that runs past the end of the video left a gap before the next one: 0.355 s for 0.3 s of sound with no picture, reported asmode: copy. It now ends with the video when it runs less than a frame past it, and otherwise holds the last frame for its sound, which re-cuts the join from the source (concat_fallback, with a note). Also,--accuratejoins went through per-part encodes and a copy join, leaving a 23 ms hole (one AAC frame) at every join; they now re-cut every segment in one encode, as the fallback does, reported only asrequested. The video's end is measured from the file's own start, so offset timestamps do not move it. - fix(cut): a
--segmentsstream-copy join is exact, or it is not a copy. On B-frame video each part carried the next GOP's keyframe and first P-frame (an input-tstops in decode order) and itsmake_zerostart shifted it by the reorder delay: 186 frames for 180, holes in the picture and the sound drifting 67 then 133 ms, reported asmode: copy(287 frames for 224 on iPhone HEVC)..mp4/.movparts of B-frame video now keep their edit list and end at their end keyframe's decode time; an end is snapped to the first keyframe at or after it, never an earlier one, within--tolerance(each end judged on its own; past it, the join is re-cut from the source), a part that reaches the end of the video keeps its length, and a later segment that starts between keyframes is re-cut (the concat demuxer crushes its hidden pre-roll into the join). A source with no B-frames (iPhone "Most Compatible" H.264) cuts its parts as 2.5.1 did, which was already frame-exact (0-1.8,4-6is 114 frames, as before). A source with open GOPs (x265's default; iPhone "High Efficiency" HEVC at every keyframe) cannot be cut exactly by stream copy, so its joins are re-cut from the source (concat_fallback, with a note), keeping HEVC, 10 bits and HDR tags. Every copy join is then measured by demuxing:join_check(packets,expected_packetscounted in the source,max_step_seconds,audio_offset_ms,audio_parts_checked,ok); each step at a join must be the one its parts predict (within 1 ms), and each part's sound is matched to the source's audio packets and must sit within 5 ms of its picture (a B-frame H.264 + PCM.movjoin copied every frame exactly with its sound 16 and 53 ms late); a failed check re-cuts the join. Only windows around the segments are read from the source (±10 s, widened up to three times, then the whole file), so a long source costs what its segments do: 0.5 s for three 1-minute segments of a 25-minute source. Matroska/MPEG-TS parts keep the old cut and rely on that check. New keysegment_end_snap_seconds. A join whose parts would re-encode is re-cut in one encode instead of copy-joining separately encoded parts.duration_error_mson an exact AAC copy join reads about +21 ms: one AAC frame of priming. A checked join is written to a hidden file beside the output and renamed into place only once it passes, so a failed check never costs an--overwritetarget; placing a finished file (this, andrender.py's last step) now holds the output's lock like any ffmpeg write. - feat(cut):
--keep-hevc(opt-in) re-encodes an SDR HEVC source as HEVC (x265 8-bit, BT.709-tagged, on VideoToolbox under--hw) instead of H.264; it covers--accurate, the tolerance fallback and the--segmentsre-cut join. Without it every SDR re-encode stays x264, as before; HDR and BT.2020 sources keep their HEVC Main10 line either way, and--codecstill overrides. The contract'scutx265 capability adds "with--keep-hevcon an HEVC source". - feat(cut):
--edit-listkeeps the MP4 edit list on a single-segment.mp4/.movstream copy instead of-avoid_negative_ts make_zero, which shows the keyframe's pre-roll (on Core Media HEVC, 3.7 s of sound with no picture from a run that exited 0): the pre-roll is stored but hidden, so the picture starts at--start. The default is unchanged (make_zero). New keys on every cut:edit_list(falsewithout the flag),stored_preroll_seconds(the hidden pre-roll, from the keyframe the demuxer really seeks to;nullwithout the flag),av_start_skew_seconds(audio start minus video start; past max(2 frames, 0.1 s) a note names--accurate, and--edit-listfor an.mp4/.movcopy cut without it: the skew is reported, not repaired) andnotes. Under--edit-lista copy is judged by its video's length, not the container's, and one that starts where asked but ends past--tolerancere-encodes without offering another--start.keyframe_snappedkeeps its meaning (a stream copy end to end); the newstart_snappedis measured:truewhen a copied picture starts more than a frame from--start(an--edit-listcopy whose edit list hides the pre-roll is not; reading a source's keyframes no longer seeks to its start, which skipped an edit-listed file's negative-pts keyframe and reported its start as snapped),nullunder--dry-run. The compact probe gains per-streamstart_time(video.start_time,audio.start_time). - fix(cut):
-ss/-tare passed to the microsecond (they were rounded to the millisecond):--start 00:00:02:01@30on a keyframe at 2.0333 s seeked to 2.033, before it, and snapped back to the keyframe at 0 (atolerancere-encode); it now copies from that keyframe. - fix(cut):
--accurateon video seeks a second early and drops the margin on the output side. Seeking straight to--startlanded, by decode time, on a keyframe after it when the start was a few frames before a keyframe on B-frame video, losing those frames. - fix(cut): a
--segmentsjoin of mismatched parts no longer goes through the concat demuxer, which takes the first part's parameters for all of them: a copied HEVC segment next to a re-encoded H.264 one decoded with errors from a run that exited 0. Parts are joined by stream copy only when their codec parameters, rotation, colour tags and extradata match; otherwise every segment is re-cut from the source through the concat filter (at most 32 per ffmpeg call), keeping an audio track's offset from the video, on the source's frame grid and at its stated frame rate (FFmpeg 7.0 otherwise wrote 25 fps). Each segment is seeked a second early and trimmed to its start, because the MP4 demuxer seeks by decode time and a start inside the B-frame reorder delay before a keyframe would land on that keyframe. A join into.mp4/.movkeeps HEVC'shvc1tag (an HDR source's re-cut is HEVC by default), including when more than 32 segments are encoded as Matroska chunks and copied together. Subtitle and data streams are dropped from that re-cut and reported (dropped_non_av_streams). - fix(cut): a video cut or
--segmentssegment shorter than one frame is refused (kind: input) before ffmpeg runs; 2.5.1 wrote a whole frame for it and reported success (--start 1 --end 1.01on 30 fps video wrote one frame, 44 ms longer than asked). A cut to an audio output is not affected. - feat(cut):
reencode_reasonsays why anything was re-encoded: a list ofrequested,codec,vfr,vfr_inconclusive,pcm_container,copy_failed,tolerance,concat_fallbackin first-seen order,[]for a clean copy.--segmentscuts addsegment_precision, and every cut addsleast_exact_precision, the least exact segment's;precisionkeeps its formula.requested_segments/requested_durationstill report the request clamped to the media's duration; a segment ended with the video shows its trim induration_delta_secondsand a note.batch.pyrows gaincut_reencode_reasons, its top level counts them incut_reencode_reasons(cut_stream_copyis unchanged), and its log no longer calls every re-encode a "hybrid tolerance fallback". - fix(cut): a
--segmentscut whose parts copied losslessly but whose join had to re-encode reportedmode: "copy",reencoded: falseandprecision: "packet"; it now reportshybrid,reencoded: true, the re-encode's precision andconcat_fallback. - The
cut.pywork above, fromreencode_reasonand theconcat_fallbackreport to the edit-list, HEVC and frame-timing paths and the exact--segmentscopy joins with theirjoin_check, was built by @rymalia in #306, measured frame by frame on fixtures and iPhone footage. The maintainers' takeover kept 2.x's defaults behind--edit-list,--keep-hevcand--vfr-guard, and added the no-B-frame path, the audio check and the bounded packet read. - feat(cut): edit-list copies, --keep-hevc, --vfr-guard and a checked join (takeover of #306) (#313)
What's Changed
Full Changelog: v2.7.0...v2.8.0