Skip to content

Instantly share code, notes, and snippets.

@ourway
Created October 7, 2025 02:07
Show Gist options
  • Save ourway/30782defe202ce7fbb88b0dd321b3ce7 to your computer and use it in GitHub Desktop.
Save ourway/30782defe202ce7fbb88b0dd321b3ce7 to your computer and use it in GitHub Desktop.
#! /usr/bin/env python3
from optparse import OptionParser
from subprocess import check_output, CalledProcessError
from shlex import quote
import sys
import os
import os.path as path
import json
import math
from mp4utils import MakeNewDir, PrintErrorAndExit
# setup main options
VERSION = "1.0.0"
SVN_REVISION = "$Revision: 539 $"
SCRIPT_PATH = path.abspath(path.dirname(__file__))
sys.path += [SCRIPT_PATH]
TempFiles = []
RESOLUTION_ROUNDING_H = 16
RESOLUTION_ROUNDING_V = 2
def scale_resolution(pixels, aspect_ratio):
x = RESOLUTION_ROUNDING_H*((int(math.ceil(math.sqrt(pixels*aspect_ratio)))+RESOLUTION_ROUNDING_H-1) // RESOLUTION_ROUNDING_H)
y = RESOLUTION_ROUNDING_V*((int(math.ceil(x/aspect_ratio))+RESOLUTION_ROUNDING_V-1) // RESOLUTION_ROUNDING_V)
return (x,y)
def compute_bitrates_and_resolutions(options):
# spread the bitrates evenly
if options.bitrates > 1:
delta = (options.max_bitrate-options.min_bitrate)/(options.bitrates-1)
else:
delta = 0
bitrates = [options.min_bitrate+delta*i for i in range(options.bitrates)]
max_pixels = options.resolution[0]*options.resolution[1]
pixels = [max_pixels*pow(bitrate/options.max_bitrate, 4.0/3.0) for bitrate in bitrates]
resolutions = [scale_resolution(x, float(options.resolution[0])/float(options.resolution[1])) for x in pixels]
bits_per_pixel = [1000.0*bitrates[i]/(24*pixels[i]) for i in range(len(pixels))]
if options.debug:
print('BITRATES:', bitrates)
print('PIXELS: ', pixels)
print('RESOLUTIONS: ', resolutions)
print('BITS PER PIXEL:', bits_per_pixel)
return (bitrates, resolutions)
def run_command(options, cmd):
if options.debug:
print('COMMAND: ', cmd)
try:
return check_output(cmd, shell=True)
except CalledProcessError as e:
message = "binary tool failed with error %d" % e.returncode
if options.verbose:
message += " - " + str(cmd)
raise Exception(message)
class MediaSource:
def __init__(self, options, filename):
self.width = 0
self.height = 0
self.frame_rate = 0
if not options.debug:
quiet = '-v quiet '
else:
quiet = ''
command = 'ffprobe -of json -loglevel quiet -show_format -show_streams ' + quiet + quote(filename)
json_probe = run_command(options, command)
self.json_info = json.loads(json_probe, strict=False)
for stream in self.json_info['streams']:
if stream['codec_type'] == 'video':
self.width = stream['width']
self.height = stream['height']
frame_rate = stream['avg_frame_rate']
if frame_rate == '0/0' or frame_rate == '0':
frame_rate = stream['r_frame_rate']
if '/' in frame_rate:
(x,y) = frame_rate.split('/')
if x and y:
self.frame_rate = float(x)/float(y)
else:
raise Exception('unable to obtain frame rate for source movie')
else:
self.frame_rate = float(frame_rate)
break
def __repr__(self):
return 'Video: resolution='+str(self.width)+'x'+str(self.height)
def main():
# parse options
global Options
parser = OptionParser(usage="%prog [options] <media-file>",
description="<media-file> is the path to a source video file. Version " + VERSION + " r" + SVN_REVISION[-5:-2])
parser.add_option('-v', '--verbose', dest="verbose", action='store_true', default=False,
help="Be verbose")
parser.add_option('-d', '--debug', dest="debug", action='store_true', default=False,
help="Print out debugging information")
parser.add_option('-k', '--keep-files', dest="keep_files", action='store_true', default=False,
help="Keep intermediate files")
parser.add_option('-o', '--output-dir', dest="output_dir",
help="Output directory", metavar="<output-dir>", default='')
parser.add_option('-b', '--bitrates', dest="bitrates",
help="Number of bitrates (default: 1)", default=1, type='int')
parser.add_option('-r', '--resolution', dest='resolution',
help="Resolution of the source video (default: auto detect)")
parser.add_option('-m', '--min-video-bitrate', dest='min_bitrate', type='float',
help="Minimum bitrate (default: 500kbps)", default=500.0)
parser.add_option('-n', '--max-video-bitrate', dest='max_bitrate', type='float',
help="Max Video bitrate (default: 2mbps)", default=2000.0)
parser.add_option('--audio-codec', dest='audio_codec', default='libfdk_aac',
help='Audio Codec: libfdk_aac (default) or aac')
parser.add_option('-c', '--video-codec', dest='video_codec', default='libx264',
help="Video Codec: libx264 (default) or libx265")
parser.add_option('-a', '--audio-bitrate', dest='audio_bitrate', type='int',
help="Audio bitrate (default: 128kbps)", default=128)
parser.add_option('', '--select-streams', dest='select_streams',
help="Only encode these streams (comma-separated list of stream indexes or stream specifiers)")
parser.add_option('-s', '--segment-size', dest='segment_size', type='int',
help="Video segment size in frames (default: 3*fps)")
parser.add_option('-t', '--text-overlay', dest='text_overlay', action='store_true', default=False,
help="Add a text overlay with the bitrate")
parser.add_option('', '--text-overlay-font', dest='text_overlay_font', default=None,
help="Specify a TTF font file to use for the text overlay")
parser.add_option('-e', '--encoder-params', dest='encoder_params',
help="Extra encoder parameters")
parser.add_option('-f', '--force', dest="force_output", action="store_true",
help="Overwrite output files if they already exist", default=False)
(options, args) = parser.parse_args()
Options = options
if len(args) == 0:
parser.print_help()
sys.exit(1)
if options.resolution:
options.resolution = [int(x) for x in options.resolution.split('x')]
if len(options.resolution) != 2:
raise Exception('ERROR: invalid value for --resolution argument')
if options.min_bitrate > options.max_bitrate:
raise Exception('ERROR: max bitrate must be >= min bitrate')
if options.output_dir:
MakeNewDir(dir=options.output_dir, exit_if_exists = not (options.force_output), severity='ERROR')
if options.verbose:
print('Encoding', options.bitrates, 'bitrates, min bitrate =', options.min_bitrate, 'max bitrate =', options.max_bitrate)
media_source = MediaSource(options, args[0])
if not options.resolution:
options.resolution = [media_source.width, media_source.height]
if options.verbose:
print('Media Source:', media_source)
if not options.segment_size:
options.segment_size = 3*int(media_source.frame_rate+0.5)
if options.bitrates == 1:
options.min_bitrate = options.max_bitrate
(bitrates, resolutions) = compute_bitrates_and_resolutions(options)
for i in range(options.bitrates):
output_filename = path.join(options.output_dir, 'video_%05d.mp4' % int(bitrates[i]))
temp_filename = output_filename+'_'
base_cmd = 'ffmpeg -i %s -strict experimental -codec:a %s -ac 2 -ab %dk -preset slow -map_metadata -1 -codec:v %s' % (quote(args[0]), options.audio_codec, options.audio_bitrate, options.video_codec)
if options.video_codec == 'libx264':
base_cmd += ' -profile:v baseline'
if options.text_overlay:
if not options.text_overlay_font:
font_file = "/Library/Fonts/Courier New.ttf"
if path.exists(font_file):
options.text_overlay_font = font_file
else:
raise Exception('ERROR: no default font file, please use the --text-overlay-font option')
if not path.exists(options.text_overlay_font):
raise Exception('ERROR: font file "'+options.text_overlay_font+'" does not exist')
base_cmd += ' -vf "drawtext=fontfile='+options.text_overlay_font+': text='+str(int(bitrates[i]))+'kbps '+str(resolutions[i][0])+'*'+str(resolutions[i][1])+': fontsize=50: x=(w)/8: y=h-(2*lh): fontcolor=white:"'
if options.select_streams:
specifiers = options.select_streams.split(',')
for specifier in specifiers:
base_cmd += ' -map 0:'+specifier
else:
base_cmd += ' -map 0'
if not options.debug:
base_cmd += ' -v quiet'
if options.force_output:
base_cmd += ' -y'
#x264_opts = "-x264opts keyint=%d:min-keyint=%d:scenecut=0:rc-lookahead=%d" % (options.segment_size, options.segment_size, options.segment_size)
#video_opts = "-g %d" % (options.segment_size)
video_opts = "-force_key_frames 'expr:eq(mod(n,%d),0)'" % (options.segment_size)
video_opts += " -bufsize %dk -maxrate %dk" % (bitrates[i], int(bitrates[i]*1.5))
if options.video_codec == 'libx264':
video_opts += " -x264opts rc-lookahead=%d" % (options.segment_size)
elif options.video_codec == 'libx265':
video_opts += ' -x265-params "no-open-gop=1:keyint=%d:no-scenecut=1:profile=main"' % (options.segment_size)
if options.encoder_params:
video_opts += ' ' + options.encoder_params
cmd = base_cmd+' '+video_opts+' -s '+str(resolutions[i][0])+'x'+str(resolutions[i][1])+' -f mp4 '+temp_filename
if options.verbose:
print('ENCODING bitrate: %d, resolution: %dx%d' % (int(bitrates[i]), resolutions[i][0], resolutions[i][1]))
run_command(options, cmd)
cmd = 'mp4fragment "%s" "%s"' % (temp_filename, output_filename)
run_command(options, cmd)
if not options.keep_files:
os.unlink(temp_filename)
###########################
if __name__ == '__main__':
Options = None # global
try:
main()
except Exception as err:
if Options and Options.debug:
raise
else:
PrintErrorAndExit('ERROR: %s\n' % str(err))
finally:
for f in TempFiles:
os.unlink(f)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment