Last active
January 20, 2026 00:18
-
-
Save FiveBoroughs/b6401ee0df041f66d62dbe3db295ff37 to your computer and use it in GitHub Desktop.
Smart HEVC/H.264/MPEG-2 Transcoding
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -euo pipefail | |
| LOG_PREFIX="[ffmpeg-smart]" | |
| # Parse args | |
| AGENT="" | |
| URL="" | |
| while [[ $# -gt 0 ]]; do | |
| if [[ "$1" == "-user_agent" ]]; then | |
| AGENT="$2" | |
| shift 2 | |
| elif [[ "$1" == "-i" ]]; then | |
| URL="$2" | |
| shift 2 | |
| else | |
| shift | |
| fi | |
| done | |
| # Validate URL | |
| if [[ -z "$URL" ]]; then | |
| echo "$LOG_PREFIX ERROR: No stream URL provided" >&2 | |
| exit 1 | |
| fi | |
| # Probe stream | |
| PROBE=$(ffprobe -user_agent "$AGENT" -v quiet -print_format json -show_streams "$URL" 2>&1) || { | |
| echo "$LOG_PREFIX ERROR: ffprobe failed - cannot access stream" >&2 | |
| exit 1 | |
| } | |
| # Parse streams by codec_type | |
| VCODEC=$(echo "$PROBE" | jq -r '.streams[] | select(.codec_type=="video") | .codec_name' | head -n1) | |
| FPS_FRAC=$(echo "$PROBE" | jq -r '.streams[] | select(.codec_type=="video") | .r_frame_rate' | head -n1) | |
| ABITRATE_RAW=$(echo "$PROBE" | jq -r '.streams[] | select(.codec_type=="audio") | .bit_rate // empty' | head -n1) | |
| # Validate video stream | |
| if [[ -z "$VCODEC" || "$VCODEC" == "null" ]]; then | |
| echo "$LOG_PREFIX ERROR: No video stream found" >&2 | |
| exit 1 | |
| fi | |
| # Set video codec | |
| if [[ "$VCODEC" == "hevc" ]]; then | |
| VCODEC_OUT="hevc_qsv" | |
| TAG_ARGS="-tag:v hvc1" | |
| elif [[ "$VCODEC" == "h264" ]]; then | |
| VCODEC_OUT="h264_qsv" | |
| TAG_ARGS="" | |
| elif [[ "$VCODEC" == "mpeg2video" ]]; then | |
| VCODEC_OUT="mpeg2_qsv" | |
| TAG_ARGS="" | |
| else | |
| VCODEC_OUT="h264_qsv" | |
| TAG_ARGS="" | |
| fi | |
| # Validate audio bitrate (reject sample rates like 44100/48000) | |
| if [[ -n "$ABITRATE_RAW" ]] && [[ "$ABITRATE_RAW" -ge 60000 ]] && [[ "$ABITRATE_RAW" -le 500000 ]]; then | |
| ABITRATE="$ABITRATE_RAW" | |
| else | |
| ABITRATE="128000" | |
| fi | |
| # Bitrates | |
| VBITRATE="8000000" | |
| MAXRATE="10000000" | |
| BUFSIZE="20000000" | |
| # Calculate GOP with rounding | |
| if [[ "$FPS_FRAC" =~ ^([0-9]+)/([0-9]+)$ ]]; then | |
| NUM=${BASH_REMATCH[1]} | |
| DEN=${BASH_REMATCH[2]} | |
| if [[ $DEN -gt 0 ]]; then | |
| GOP=$(( (NUM + DEN/2) / DEN )) | |
| FPS_OUT="$FPS_FRAC" | |
| GOP_WARN="" | |
| else | |
| GOP=50 | |
| FPS_OUT="25/1" | |
| GOP_WARN=" (invalid fps denominator)" | |
| fi | |
| else | |
| GOP=50 | |
| FPS_OUT="25/1" | |
| GOP_WARN=" (fps parse failed)" | |
| fi | |
| # Single combined log line | |
| echo "$LOG_PREFIX Detected $VCODEC @ $FPS_FRAC -> $VCODEC_OUT GOP=$GOP${GOP_WARN} audio=${ABITRATE}bps" >&2 | |
| # Execute ffmpeg | |
| exec ffmpeg \ | |
| -user_agent "$AGENT" \ | |
| -hwaccel qsv \ | |
| -hwaccel_output_format qsv \ | |
| -reconnect 1 \ | |
| -reconnect_at_eof 1 \ | |
| -reconnect_streamed 1 \ | |
| -reconnect_delay_max 30 \ | |
| -rw_timeout 15000000 \ | |
| -fflags +genpts+igndts+discardcorrupt \ | |
| -err_detect ignore_err \ | |
| -i "$URL" \ | |
| -map 0:v:0 \ | |
| -map 0:a:0? \ | |
| -c:v "$VCODEC_OUT" \ | |
| -preset fast \ | |
| -b:v "$VBITRATE" \ | |
| -maxrate "$MAXRATE" \ | |
| -bufsize "$BUFSIZE" \ | |
| -g "$GOP" \ | |
| -bf 0 \ | |
| -look_ahead 0 \ | |
| -fps_mode cfr \ | |
| -r "$FPS_OUT" \ | |
| -async 1 \ | |
| $TAG_ARGS \ | |
| -c:a aac \ | |
| -b:a "$ABITRATE" \ | |
| -af "aresample=async=1" \ | |
| -avoid_negative_ts make_zero \ | |
| -start_at_zero \ | |
| -mpegts_copyts 0 \ | |
| -mpegts_flags +pat_pmt_at_frames+resend_headers \ | |
| -flush_packets 1 \ | |
| -max_muxing_queue_size 4096 \ | |
| -f mpegts \ | |
| pipe:1 |
Author
Author
Revision 3
Audio bitrate validation:
Added 60k-500k range check to reject corrupt metadata
Fixes streams with sample_rate (44100/48000) incorrectly set as bit_rate
Falls back to 128k for invalid/missing values
Author
Revision 4
ffprobe now uses -user_agent "$AGENT".
Added TAG_ARGS="-tag:v hvc1" for HEVC output (empty for others).
GOP calc now guards against DEN=0 and also derives FPS_OUT (defaults to 25/1 when parsing fails).
ffmpeg changes:
-fflags adds +igndts
audio map is optional now: -map 0:a:0?
forces CFR: -fps_mode cfr -r "$FPS_OUT"
adds audio resample async filter: -af "aresample=async=1"
adds timestamp/TS stabilization: -avoid_negative_ts make_zero -start_at_zero -mpegts_copyts 0
formatted as multi-line for readability
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Revision 2
Robustness:
Added set -euo pipefail - fail fast on errors
Error handling for missing URL, probe failures, no video stream
Stream selection by codec_type (not array position) - handles multi-audio/reordered streams
Explicit stream mapping
Codec Support:
Added MPEG-2 → mpeg2_qsv
Added fallback for unknown codecs → h264_qsv
Math Fix:
GOP calculation uses rounding not truncation (30000/1001 → 30, not 29)
Logging:
Single-line [ffmpeg-smart] prefixed log with probe results
Shows: detected codec, FPS, target encoder, GOP, audio bitrate
Only logs errors/warnings when they occur
Result: Handles edge cases, multi-stream inputs, and all common broadcast codecs while providing debug visibility.