ffmpeg-skill 3.6.1 → 3.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,7 @@ import argparse
19
19
  import sys
20
20
  from typing import List, Tuple
21
21
 
22
- from _common.decision import av_trim, frame_rate, grid_at_or_after, stream_starts
22
+ from _common.decision import av_trim, frame_durations, frame_rate, grid_at_or_after, stream_starts
23
23
  from _common import add_common, apply_common, aac_args, default_output, die, emit, ffmpeg_base, info, probe, run, video_args, X264_PRESETS, fmt_secs, time_arg
24
24
 
25
25
  MAX_SPEED = 20.0
@@ -106,7 +106,11 @@ def main() -> int:
106
106
  die(f"--segment {start:g}-{end:g}:{factor:g} holds no frame: both ends fall on the frame at "
107
107
  f"{bounds[i]:.3f}s; widen it or merge it into a neighbour")
108
108
  vtrim, atrim = av_trim(bounds[i], seg_end, rate)
109
- vparts.append(f"[0:v]{vtrim},setpts=PTS/{factor:.6f}[{vlabel}]")
109
+ # the closing fps puts the sped-up frames back on the source's grid with its frame
110
+ # duration: concat starts the next segment after the last frame's duration, which setpts
111
+ # leaves at the unscaled 1/rate, so every 2x segment pushed the rest of the picture two
112
+ # frames late against the sound (and added those frames)
113
+ vparts.append(f"[0:v]{vtrim},setpts=PTS/{factor:.6f},{frame_durations(meta, rate)}[{vlabel}]")
110
114
  labels.append(f"[{vlabel}]")
111
115
  if has_audio:
112
116
  aparts.append(f"[0:a]{atrim},{atempo_chain(factor)}[{alabel}]")
package/scripts/sync.py CHANGED
@@ -35,7 +35,8 @@ from typing import Dict, List
35
35
  from _common.runner import fail_if_classified
36
36
  from _common import video_args, add_common, apply_common, emit, aac_args, audio_codec_for, default_output, die, ffmpeg_base, info, probe, require_tool, run, run_analysis, x264_args, decode_pcm_mono, rms_envelope
37
37
  from _common.emit import record_skip, warn
38
- from _common.decision import stream_starts
38
+ from _common.decision import SEEK_MARGIN, frame_rate, grid_at_or_after, stream_starts
39
+ from _common.timeline import output_timecode_args
39
40
 
40
41
  SR = 8000 # decode sample rate
41
42
 
@@ -385,7 +386,12 @@ def main() -> int:
385
386
  cmd = cmd[:-1] + video_args(probe(args.reference) if args.replace_audio else probe(args.second), args.crf) + [output]
386
387
  run(cmd)
387
388
  else:
388
- if head_trim > 0 and not drift_af:
389
+ # A picture is never stream-copied here: a copy starts at the keyframe at or before -ss
390
+ # and -avoid_negative_ts make_zero shows the frames from it, so the output started
391
+ # early by up to a GOP (a 5 s GOP: nothing trimmed at all) and was reported as synced.
392
+ # Even a keyframe on the trim point moved the picture by its B-frame delay (+69 ms).
393
+ # The re-encode below starts on the frame itself; a sound-only file is still copied.
394
+ if head_trim > 0 and not drift_af and not has_video:
389
395
  cmd = ffmpeg_base() + ["-ss", f"{head_trim:.4f}", "-i", args.second, "-c", "copy"]
390
396
  if os.path.splitext(output)[1].lower() in (".mp4", ".mov", ".m4v"):
391
397
  cmd += ["-movflags", "+faststart"]
@@ -396,10 +402,20 @@ def main() -> int:
396
402
  cmd = ffmpeg_base() + ["-ss", f"{head_trim:.4f}", "-i", args.second] + (video_args(probe(args.reference) if args.replace_audio else probe(args.second), args.crf) if has_video else []) + audio_codec_for(output) + [output]
397
403
  run(cmd)
398
404
  else:
405
+ # Seek SEEK_MARGIN early and drop the margin on the output side, as cut.py's
406
+ # re-encode does: an input seek straight to head_trim can land (the MP4 demuxer
407
+ # seeks over raw decode times) on a keyframe whose picture comes after it, and a
408
+ # B-frame HEVC file lost the frames before it. The output-side -ss decodes and
409
+ # drops the margin from both streams alike; it counts on the output's clock, which
410
+ # a drift fix runs faster by drift_ratio (setpts and asetrate alike).
399
411
  cmd = ffmpeg_base()
412
+ out_seek: List[str] = []
400
413
  if head_trim > 0:
401
- cmd += ["-ss", f"{head_trim:.4f}"]
402
- cmd += ["-i", args.second]
414
+ m = min(SEEK_MARGIN, head_trim)
415
+ cmd += ["-ss", f"{head_trim - m:.6f}"]
416
+ if m > 0:
417
+ out_seek = ["-ss", f"{m / (drift_ratio if drift_af else 1.0):.6f}"]
418
+ cmd += ["-i", args.second] + out_seek
403
419
  af_parts = on_clock + drift_af
404
420
  if delay_ms > 0:
405
421
  af_parts.append(f"adelay={delay_ms}:all=1")
@@ -412,6 +428,14 @@ def main() -> int:
412
428
  if vf:
413
429
  cmd += ["-vf", ",".join(vf)]
414
430
  cmd += video_args(probe(args.reference) if args.replace_audio else probe(args.second), args.crf)
431
+ # the output's first frame is the second file's frame at head_trim, or (padded)
432
+ # one `offset` before its first: its start timecode moves with it
433
+ v0, rate = stream_starts(second_meta)[0], frame_rate(second_meta)
434
+ # (a trim before a picture that starts after the file's origin starts on its
435
+ # first frame; only the padded head counts back before it)
436
+ first = (grid_at_or_after(max(round(head_trim, 4), v0), v0, rate) - v0 if head_trim > 0
437
+ else -offset if delay_ms > 0 else 0.0)
438
+ cmd += output_timecode_args(second_meta, first)
415
439
  if af_parts:
416
440
  cmd += ["-af", ",".join(af_parts)]
417
441
  cmd += audio_codec_for(output) + [output]