Skip to content

v2.8.0

Latest

Choose a tag to compare

@github-actions github-actions released this 09 Oct 23:47

Automated release: version and notes generated from pull requests merged since 2.7.0.

  • feat(cut): --vfr-guard average|sampled|off chooses 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 reason vfr); its packet timestamps are then sampled in up to five 6 s windows (no decoding) and reported in vfr_check (measured: sampled_cfr / vfr / inconclusive, guard, windows, deltas), with a note naming --vfr-guard sampled when the sample is constant, as on an iPhone clip at 29.98 against a nominal 30. sampled decides on the sample instead: sampled_cfr copies, vfr re-encodes with reason vfr and inconclusive (too few frames, or ffprobe failed) with vfr_inconclusive; off samples and keeps the copy, with a note. Variable timing between the sampled windows is not seen. The sample runs under --dry-run and is skipped when nothing would copy (audio-only, --accurate, --codec). probe.py's variable_frame_rate_suspected keeps its meaning.
  • fix(cut): a --segments segment 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 as mode: 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, --accurate joins 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 as requested. The video's end is measured from the file's own start, so offset timestamps do not move it.
  • fix(cut): a --segments stream-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 -t stops in decode order) and its make_zero start shifted it by the reorder delay: 186 frames for 180, holes in the picture and the sound drifting 67 then 133 ms, reported as mode: copy (287 frames for 224 on iPhone HEVC). .mp4/.mov parts 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-6 is 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_packets counted 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 .mov join 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 key segment_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_ms on 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 --overwrite target; placing a finished file (this, and render.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 --segments re-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 --codec still overrides. The contract's cut x265 capability adds "with --keep-hevc on an HEVC source".
  • feat(cut): --edit-list keeps the MP4 edit list on a single-segment .mp4/.mov stream 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 (false without the flag), stored_preroll_seconds (the hidden pre-roll, from the keyframe the demuxer really seeks to; null without the flag), av_start_skew_seconds (audio start minus video start; past max(2 frames, 0.1 s) a note names --accurate, and --edit-list for an .mp4/.mov copy cut without it: the skew is reported, not repaired) and notes. Under --edit-list a copy is judged by its video's length, not the container's, and one that starts where asked but ends past --tolerance re-encodes without offering another --start. keyframe_snapped keeps its meaning (a stream copy end to end); the new start_snapped is measured: true when a copied picture starts more than a frame from --start (an --edit-list copy 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), null under --dry-run. The compact probe gains per-stream start_time (video.start_time, audio.start_time).
  • fix(cut): -ss/-t are passed to the microsecond (they were rounded to the millisecond): --start 00:00:02:01@30 on a keyframe at 2.0333 s seeked to 2.033, before it, and snapped back to the keyframe at 0 (a tolerance re-encode); it now copies from that keyframe.
  • fix(cut): --accurate on video seeks a second early and drops the margin on the output side. Seeking straight to --start landed, 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 --segments join 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/.mov keeps HEVC's hvc1 tag (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 --segments segment 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.01 on 30 fps video wrote one frame, 44 ms longer than asked). A cut to an audio output is not affected.
  • feat(cut): reencode_reason says why anything was re-encoded: a list of requested, codec, vfr, vfr_inconclusive, pcm_container, copy_failed, tolerance, concat_fallback in first-seen order, [] for a clean copy. --segments cuts add segment_precision, and every cut adds least_exact_precision, the least exact segment's; precision keeps its formula. requested_segments/requested_duration still report the request clamped to the media's duration; a segment ended with the video shows its trim in duration_delta_seconds and a note. batch.py rows gain cut_reencode_reasons, its top level counts them in cut_reencode_reasons (cut_stream_copy is unchanged), and its log no longer calls every re-encode a "hybrid tolerance fallback".
  • fix(cut): a --segments cut whose parts copied losslessly but whose join had to re-encode reported mode: "copy", reencoded: false and precision: "packet"; it now reports hybrid, reencoded: true, the re-encode's precision and concat_fallback.
  • The cut.py work above, from reencode_reason and the concat_fallback report to the edit-list, HEVC and frame-timing paths and the exact --segments copy joins with their join_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-hevc and --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

  • cut.py: exact --segments copy joins, and reports that say what happened by @kajisho5 in #313

Full Changelog: v2.7.0...v2.8.0