ffmpeg-skill 2.24.1 → 3.0.0
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.
- package/README.md +12 -13
- package/SKILL.md +4 -4
- package/bin/install.js +24 -29
- package/docs/contract.md +61 -33
- package/package.json +1 -1
- package/references/ci-platform-pitfalls.md +7 -4
- package/references/devices.md +2 -2
- package/references/gotchas.md +11 -9
- package/references/scripts.md +21 -18
- package/scripts/_common/drawtext.py +6 -10
- package/scripts/_common/probe.py +41 -3
- package/scripts/_common/runner.py +21 -1
- package/scripts/_contract.py +69 -59
- package/scripts/cut.py +46 -29
package/scripts/cut.py
CHANGED
|
@@ -13,15 +13,16 @@ WAV), -o out.m4a writes AAC; an audio extension on a video input drops the
|
|
|
13
13
|
picture (mp4 -> wav extraction). The result reports `precision`
|
|
14
14
|
(packet / sample / codec_frame / frame) and the measured duration error, plus
|
|
15
15
|
`mode` (copy / accurate / hybrid -- "hybrid" means a lossless cut silently
|
|
16
|
-
re-encoded because the keyframe snap exceeded --tolerance), `keyframe_snapped`
|
|
17
|
-
|
|
16
|
+
re-encoded because the keyframe snap exceeded --tolerance), `keyframe_snapped` and
|
|
17
|
+
`start_snapped` (measured: a copied start moved past a frame; equal since 3.0.0),
|
|
18
18
|
`reencode_reason`, `requested_start`/`requested_end` (or `requested_segments` for --segments),
|
|
19
19
|
`requested_duration`, `output_duration` and `duration_delta_seconds` -- so a
|
|
20
20
|
caller never has to trust "it probably cut where I asked" on faith.
|
|
21
21
|
|
|
22
|
-
A single-segment .mp4/.mov copy
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
A single-segment .mp4/.mov copy keeps the MP4 edit list (the default since 3.0.0),
|
|
23
|
+
which hides the keyframe's pre-roll so the picture starts at --start; --no-edit-list
|
|
24
|
+
shifts its timestamps to zero instead (-avoid_negative_ts make_zero, every 2.x
|
|
25
|
+
release's default), so the picture starts at the keyframe.
|
|
25
26
|
An --accurate re-encode of a video does not: make_zero moved the picture by the
|
|
26
27
|
AAC priming (about 21 ms, and further when a B-frame's decode time is negative)
|
|
27
28
|
while the sound stayed at 0. A start that lands on a frame keeps that frame at 0.
|
|
@@ -29,14 +30,16 @@ A start between frames begins on the next frame and may keep up to one frame of
|
|
|
29
30
|
timestamp. The MP4 edit list hides the encoder priming, and a source whose audio
|
|
30
31
|
starts late keeps that offset.
|
|
31
32
|
|
|
32
|
-
A re-encode
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
A re-encode keeps an SDR HEVC source HEVC (x265 8-bit BT.709, or VideoToolbox under --hw;
|
|
34
|
+
the default since 3.0.0), so a trimmed phone clip keeps its codec; --no-keep-hevc uses x264
|
|
35
|
+
as every 2.x release did. Other SDR sources get x264 and an HDR or BT.2020 one x265 Main10,
|
|
36
|
+
as every editing tool does. --codec overrides all of these.
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
The VFR guard samples a video's packet timing (`vfr_check`) and re-encodes, as --accurate,
|
|
39
|
+
only when it is variable or too short to judge (--vfr-guard sampled, the default since
|
|
40
|
+
3.0.0). --vfr-guard average is every 2.x release's: a source whose nominal and average frame
|
|
41
|
+
rates differ is taken for variable-frame-rate and re-encoded. --vfr-guard off keeps the copy
|
|
42
|
+
with a note.
|
|
40
43
|
|
|
41
44
|
Examples:
|
|
42
45
|
python3 cut.py input.mp4 --start 00:00:10 --end 00:00:25
|
|
@@ -173,11 +176,10 @@ def least_exact(precisions: List[str]) -> str:
|
|
|
173
176
|
def report_precision(meta: dict, dst: str, reencoded: bool, outcomes: List[dict]) -> dict:
|
|
174
177
|
"""The run's precision keys from its segments' outcomes.
|
|
175
178
|
|
|
176
|
-
precision
|
|
177
|
-
|
|
178
|
-
keyframe_snapped
|
|
179
|
-
|
|
180
|
-
really moved is start_snapped
|
|
179
|
+
precision since 3.0.0, the least exact segment's precision (least_exact_precision);
|
|
180
|
+
2.x reported precision_of() for the whole run
|
|
181
|
+
keyframe_snapped since 3.0.0, start_snapped (a copied start really moved); 2.x reported
|
|
182
|
+
"the run is a stream copy end to end" (precision_of() == "packet")
|
|
181
183
|
start_snapped measured: true when a copied segment's picture starts more than a frame from
|
|
182
184
|
its requested start; false when every start is where asked (an edit list, a
|
|
183
185
|
start on a keyframe, an audio-only copy's packet seek) or everything
|
|
@@ -186,7 +188,6 @@ def report_precision(meta: dict, dst: str, reencoded: bool, outcomes: List[dict]
|
|
|
186
188
|
an audio-only --segments join of copied and re-encoded parts (a video join
|
|
187
189
|
re-cuts every segment when any part re-encodes)
|
|
188
190
|
segment_precision each segment's, for --segments (null for one segment)"""
|
|
189
|
-
precision = precision_of(meta, dst, reencoded)
|
|
190
191
|
snaps = [o.get("start_snapped") for o in outcomes]
|
|
191
192
|
if any(s is True for s in snaps):
|
|
192
193
|
start_snapped = True
|
|
@@ -194,8 +195,11 @@ def report_precision(meta: dict, dst: str, reencoded: bool, outcomes: List[dict]
|
|
|
194
195
|
start_snapped = None
|
|
195
196
|
else:
|
|
196
197
|
start_snapped = False
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
least = least_exact([o["precision"] for o in outcomes])
|
|
199
|
+
# 3.0.0: `precision` is the least exact segment's and `keyframe_snapped` says whether a copied
|
|
200
|
+
# start really moved (2.x: the run-level formula, and "the run is a stream copy end to end")
|
|
201
|
+
return {"precision": least, "keyframe_snapped": start_snapped, "start_snapped": start_snapped,
|
|
202
|
+
"least_exact_precision": least,
|
|
199
203
|
"segment_precision": [o["precision"] for o in outcomes] if len(outcomes) > 1 else None}
|
|
200
204
|
|
|
201
205
|
|
|
@@ -1308,13 +1312,24 @@ def main() -> int:
|
|
|
1308
1312
|
help="the inverse of --segments: drop these ranges and keep the rest, joined in order "
|
|
1309
1313
|
"(e.g. '1:20-2:05,10:00-10:30'); overlapping ranges merge")
|
|
1310
1314
|
ap.add_argument("--accurate", action="store_true", help="always re-encode for frame-accurate (video) / sample-accurate (audio) cuts (default: lossless -c copy, re-encoding only when the keyframe snap exceeds --tolerance)")
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
"
|
|
1315
|
+
el = ap.add_mutually_exclusive_group()
|
|
1316
|
+
el.add_argument("--edit-list", action="store_true",
|
|
1317
|
+
help="a single-segment .mp4/.mov stream copy keeps the MP4 edit list that hides the keyframe's pre-roll, "
|
|
1318
|
+
"so the picture starts at --start (the default since 3.0.0); a player that ignores edit lists shows the pre-roll")
|
|
1319
|
+
el.add_argument("--no-edit-list", action="store_true",
|
|
1320
|
+
help="shift the copy's timestamps instead (-avoid_negative_ts make_zero, every 2.x release's default): "
|
|
1321
|
+
"the picture starts at the keyframe before --start")
|
|
1322
|
+
kh = ap.add_mutually_exclusive_group()
|
|
1323
|
+
kh.add_argument("--keep-hevc", action="store_true",
|
|
1324
|
+
help="a re-encode of an SDR HEVC source stays HEVC (x265 8-bit BT.709; VideoToolbox under --hw); "
|
|
1325
|
+
"the default since 3.0.0; --codec overrides it")
|
|
1326
|
+
kh.add_argument("--no-keep-hevc", action="store_true",
|
|
1327
|
+
help="re-encode an SDR HEVC source with x264, as every 2.x release did (HDR still gets x265 Main10)")
|
|
1328
|
+
ap.add_argument("--vfr-guard", choices=["average", "sampled", "off"], default="sampled",
|
|
1329
|
+
help="what keeps a variable-frame-rate source from a stream copy (default sampled since 3.0.0: re-encode "
|
|
1330
|
+
"only when sampled packet timestamps are variable or too few to judge; average, every 2.x "
|
|
1331
|
+
"release's default: a nominal rate that differs from the average re-encodes, as --accurate, "
|
|
1332
|
+
"and the sampled timing is reported; off: measure and keep the copy, with a note)")
|
|
1318
1333
|
ap.add_argument("--tolerance", type=float, default=0.5, help="max seconds a lossless cut may deviate before re-encoding kicks in (default 0.5, -1 = never for a "
|
|
1319
1334
|
"keyframe snap; a --segments join is still re-cut for open GOPs, a later B-frame .mp4/.mov "
|
|
1320
1335
|
"segment starting between keyframes, or a failed join check)")
|
|
@@ -1340,7 +1355,9 @@ def main() -> int:
|
|
|
1340
1355
|
args = ap.parse_args()
|
|
1341
1356
|
apply_common(args)
|
|
1342
1357
|
global KEEP_HEVC
|
|
1343
|
-
|
|
1358
|
+
# 3.0.0 defaults: keep HEVC and the edit list unless the --no- form is given (--edit-list and
|
|
1359
|
+
# --keep-hevc stay accepted, and now say what happens anyway)
|
|
1360
|
+
KEEP_HEVC = not args.no_keep_hevc
|
|
1344
1361
|
|
|
1345
1362
|
meta = probe(args.input)
|
|
1346
1363
|
total = meta.get("duration") or 0.0
|
|
@@ -1492,7 +1509,7 @@ def main() -> int:
|
|
|
1492
1509
|
join_check = None
|
|
1493
1510
|
if len(segments) == 1:
|
|
1494
1511
|
outcomes.append(cut_one(args.input, segments[0][0], segments[0][1], output, args.accurate, args.crf, args.preset, args.tolerance, meta,
|
|
1495
|
-
edit_list_ok=args.
|
|
1512
|
+
edit_list_ok=not args.no_edit_list))
|
|
1496
1513
|
elif hold_needed or args.accurate:
|
|
1497
1514
|
# Straight to one encode through the concat filter. A held frame needs the re-cut (no part
|
|
1498
1515
|
# can carry it into a copy join); and when every part re-encodes anyway, encoding each on
|