aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDennis Fink2026-05-08 22:16:51 +0200
committerDennis Fink2026-05-08 22:16:51 +0200
commit4b5967b1fde15f0d7d677274f1ae353915e6854b (patch)
tree678962fff66a97789386b298cdb6c8e8cb1c13bf
parentd3ee4e3e82ea5433e467331a2605675be2a60c9f (diff)
downloadtranscode.sh-4b5967b1fde15f0d7d677274f1ae353915e6854b.tar.gz
transcode.sh-4b5967b1fde15f0d7d677274f1ae353915e6854b.zip
feat(core): expose frame-rate variables to presets
Add input_frame_rate, input_fps, and output_gop_size to the set of variables available to preset files at source time. input_frame_rate is the raw ffprobe fraction (e.g. 30000/1001). input_fps rounds that fraction to the nearest integer via awk. output_gop_size is derived as roughly five seconds of video (input_fps * 5), capped at 300 frames. The ffprobe invocation is extended to request r_frame_rate alongside the existing codec_name and pix_fmt entries; a missing frame rate is now treated as a hard error. Dry-run and verbose output are updated to display the new values. Documentation (header comment, --help text, man page, README) is updated accordingly.
Diffstat (limited to '')
-rw-r--r--README.md3
-rwxr-xr-xtranscode.sh54
-rw-r--r--transcode.sh.126
3 files changed, 79 insertions, 4 deletions
diff --git a/README.md b/README.md
index bc9945d..95ffb9b 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,9 @@ The following variables are available to presets at source time:
| `input_codec` | Video codec of the input file (e.g. `h264`, `hevc`) |
| `input_pixel_format` | Pixel format of the input after `yuvj*` normalisation (e.g. `yuv420p`) |
| `output_pixel_format` | Recommended output pixel format, derived from the input to preserve chroma subsampling and bit depth |
+| `input_frame_rate` | Raw frame rate fraction of the input file as reported by `ffprobe` (e.g. `30000/1001`, `25/1`) |
+| `input_fps` | `input_frame_rate` rounded to the nearest integer (e.g. `30`, `25`, `60`) |
+| `output_gop_size` | Recommended GOP size derived from `input_fps` (≈ 5 s of video, capped at 300 frames) |
Before the preset's `ffargs` are appended, the script always passes
these base arguments to `ffmpeg`:
diff --git a/transcode.sh b/transcode.sh
index 00c7bd5..9d66f49 100755
--- a/transcode.sh
+++ b/transcode.sh
@@ -20,6 +20,12 @@
# input_pixel_format - pixel format after yuvj* normalisation (e.g. yuv420p)
# output_pixel_format - recommended output pixel format, derived from the
# input to preserve chroma subsampling and bit depth
+# input_frame_rate - raw frame rate fraction as reported by ffprobe
+# (e.g. 30000/1001, 25/1, 60/1)
+# input_fps - input_frame_rate rounded to nearest integer
+# (e.g. 30, 25, 60)
+# output_gop_size - recommended GOP size derived from input_fps
+# (≈ 5 s of video, capped at 300 frames)
#
# Note: Presets are sourced as shell code. Only use trusted presets.
#
@@ -112,7 +118,7 @@ set -o pipefail
readonly SCRIPTNAME=${0##*/}
readonly DESCRIPTION="Batch transcode helper for media files using ffmpeg."
readonly DATE_OF_CREATION=2025-08-06
-readonly DATE_OF_REVISION=2026-02-25
+readonly DATE_OF_REVISION=2026-05-08
readonly VERSION=1.0.1
readonly AUTHOR="Dennis Fink <me+coding@dennisfink.me>"
readonly LICENSE="BSD-3-Clause"
@@ -266,6 +272,9 @@ ${BOLD}${BLUE}Configuration files:${ALL_OFF}
${BOLD}${GREEN}input_codec${ALL_OFF} e.g. h264, hevc
${BOLD}${GREEN}input_pixel_format${ALL_OFF} e.g. yuv420p
${BOLD}${GREEN}output_pixel_format${ALL_OFF} derived from input
+ ${BOLD}${GREEN}input_frame_rate${ALL_OFF} raw fraction from ffprobe (e.g. 30000/1001)
+ ${BOLD}${GREEN}input_fps${ALL_OFF} input_frame_rate rounded to nearest integer
+ ${BOLD}${GREEN}output_gop_size${ALL_OFF} ~5 s GOP derived from input_fps, capped at 300
${BOLD}${RED}presets are sourced as shell code; only use trusted presets from trusted
directories.${ALL_OFF}
@@ -560,7 +569,8 @@ parse_skip_codec_parameter() {
# Contract:
# The sourced preset MUST set the bash array `ffargs`.
# Presets may reference variables from the caller (encode_one) such as:
-# output_pixel_format, input_pixel_format, input_codec
+# output_pixel_format, input_pixel_format, input_codec, input_frame_rate,
+# input_fps, output_gop_size
load_preset() {
local preset="$1"
# Strict allowlist: only alphanumerics, hyphens, and underscores are permitted.
@@ -923,12 +933,16 @@ encode_one() {
# the startup overhead on large batches.
local probe_out
probe_out=$(ffprobe -v error -select_streams v:0 \
- -show_entries stream=codec_name,pix_fmt \
+ -show_entries stream=codec_name,pix_fmt,r_frame_rate \
-of default=nk=1:nw=1 "file:$file" 2>/dev/null)
- local input_codec input_pixel_format
+ local input_codec input_pixel_format input_frame_rate input_fps
+
input_codec=$(printf '%s\n' "$probe_out" | sed -n '1p')
+ debug "Input codec:" "$input_codec"
+
input_pixel_format=$(printf '%s\n' "$probe_out" | sed -n '2p')
+ input_frame_rate=$(printf "%s\n" "$probe_out" | sed -n '3p')
if [[ -z "$input_codec" ]]; then
error "Could not determine input video codec:" "$file"
@@ -938,6 +952,10 @@ encode_one() {
error "Could not determine input pixel format:" "$file"
return $EXIT_RUNTIME_FAILURE
fi
+ if [[ -z "$input_frame_rate" ]]; then
+ error "Could not determine input frame rate:" "$file"
+ return $EXIT_RUNTIME_FAILURE
+ fi
if [[ -v SKIP_CODECS["$input_codec"] ]]; then
msg "Skipping (codec excluded):" "$input_codec"
@@ -975,6 +993,31 @@ encode_one() {
output_pixel_format="yuv${subsampling}${depth}"
debug "Selected output pixel format:" "$output_pixel_format"
+ # Convert ffprobe frame rate fractions like 30000/1001 to rounded integer FPS
+ # values for simpler encoder heuristics and logging.
+ input_fps=$(
+ awk -F/ '
+ {
+ if ($2)
+ printf "%.0f\n", $1 / $2
+ else
+ printf "%.0f\n", $1
+ }
+ ' <<<"$input_frame_rate"
+ )
+ debug "Calculated input fps:" "$input_fps"
+
+ # Use a larger GOP for better compression efficiency while keeping seek
+ # performance reasonable. A common rule-of-thumb is roughly five seconds of
+ # video per GOP, capped at 300 frames.
+ local output_gop_size
+ output_gop_size=$(((10 * input_fps) / 2))
+ if [[ "$output_gop_size" -ge 300 ]]; then
+ debug "Calculated GOP size exceeds 300 frames, capping to 300"
+ output_gop_size=300
+ fi
+ debug "Calculated output GOP size:" "$output_gop_size"
+
load_preset "$PRESET_NAME"
local extension directory tmp base stem
@@ -1027,6 +1070,9 @@ encode_one() {
verbose "Input codec:" "$input_codec"
verbose "Input pixel format:" "$input_pixel_format"
verbose "Output pixel format:" "$output_pixel_format"
+ verbose "Input frame rate:" "$input_frame_rate"
+ verbose "Input FPS:" "$input_fps"
+ verbose "Output GOP size:" "$output_gop_size"
verbose "Command:" "${cmd[*]}"
return $EXIT_OK
fi
diff --git a/transcode.sh.1 b/transcode.sh.1
index 8fe1e90..9a4c894 100644
--- a/transcode.sh.1
+++ b/transcode.sh.1
@@ -195,6 +195,32 @@ normalisation (e.g.\&
The recommended output pixel format, automatically derived from the
input to preserve chroma subsampling (4:2:0, 4:2:2, 4:4:4) and bit
depth (8, 10, 12, 16\-bit).
+.TP
+.B input_frame_rate
+The raw frame rate of the input video stream as reported by
+.BR ffprobe (1),
+expressed as a fraction (e.g.\&
+.IR 30000/1001 ,
+.IR 25/1 ,
+.IR 60/1 ).
+.TP
+.B input_fps
+The input frame rate rounded to the nearest integer (e.g.\&
+.IR 30 ,
+.IR 25 ,
+.IR 60 ).
+Derived from
+.B input_frame_rate
+and provided as a convenience for encoder heuristics that require a whole
+number.
+.TP
+.B output_gop_size
+The recommended GOP (Group of Pictures) size for the output, derived from
+.B input_fps
+as approximately five seconds of video (capped at 300 frames).
+Use this in presets to set
+.B \-g
+for encoders that accept an explicit GOP size.
.RE
.IP
Before the preset's