Skip to content

Instantly share code, notes, and snippets.

@twobob
Created October 28, 2024 19:47
Show Gist options
  • Save twobob/ba30a21e763a255d5b8dc0643ab537b6 to your computer and use it in GitHub Desktop.
Save twobob/ba30a21e763a255d5b8dc0643ab537b6 to your computer and use it in GitHub Desktop.
aim to hit a size without going over for mp4 files
import argparse
import subprocess
import os
from pathlib import Path
def compress_video(input_file, output_file, target_size_mb=5, initial_bitrate=500,
initial_scale=640, min_bitrate=50, min_scale=100, audio_bitrate=96,
max_attempts=5):
"""
Compress a video file to a target size while adjusting bitrate and scale.
Args:
input_file (str): Path to input video file.
output_file (str): Path to output video file.
target_size_mb (float): Target file size in megabytes.
initial_bitrate (int): Starting video bitrate in kbps.
initial_scale (int): Starting width in pixels.
min_bitrate (int): Minimum allowed video bitrate in kbps.
min_scale (int): Minimum allowed width in pixels.
audio_bitrate (int): Audio bitrate in kbps.
max_attempts (int): Maximum number of attempts to reach target size.
"""
# Convert target size to bits
target_size_bits = target_size_mb * 1024 * 1024 * 8 # Convert MB to bits
# Get the duration of the video in seconds
duration_cmd = [
'ffprobe', '-v', 'error', '-show_entries',
'format=duration', '-of',
'default=noprint_wrappers=1:nokey=1', str(input_file)
]
try:
duration = float(subprocess.check_output(duration_cmd, stderr=subprocess.DEVNULL).decode().strip())
except subprocess.CalledProcessError:
print("Error: Unable to get video duration.")
return
# Initial estimation of total bitrate required (in kbps)
total_bitrate = (target_size_bits / duration) / 1000 # Convert bits/sec to kbps
video_bitrate = int(total_bitrate - audio_bitrate)
if video_bitrate < min_bitrate:
video_bitrate = min_bitrate
scale = initial_scale
for attempt in range(1, max_attempts + 1):
cmd = [
'ffmpeg', '-y', '-i', str(input_file),
'-b:v', f'{video_bitrate}k',
'-b:a', f'{audio_bitrate}k',
'-vf', f'scale={scale}:-1',
'-pass', '1', '-f', 'mp4', os.devnull
]
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
cmd = [
'ffmpeg', '-y', '-i', str(input_file),
'-b:v', f'{video_bitrate}k',
'-b:a', f'{audio_bitrate}k',
'-vf', f'scale={scale}:-1',
'-pass', '2', str(output_file)
]
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# Get the size of the output file
size = os.path.getsize(output_file)
size_mb = size / (1024 * 1024)
print(f'Attempt {attempt}: Size={size_mb:.2f}MB | Video Bitrate={video_bitrate}k | Scale={scale}px')
# Check if the size is under or close to the target size
if target_size_mb * 0.95 <= size_mb <= target_size_mb * 1.05:
print(f'Final size achieved: {size_mb:.2f}MB')
break
elif size_mb > target_size_mb:
# Decrease bitrate and/or scale
bitrate_ratio = target_size_mb / size_mb
video_bitrate = max(int(video_bitrate * bitrate_ratio * 0.95), min_bitrate)
scale = max(scale - 100, min_scale)
if video_bitrate <= min_bitrate and scale <= min_scale:
print('Cannot reduce video bitrate or scale further without significant quality loss.')
break
else:
# Increase bitrate
bitrate_ratio = target_size_mb / size_mb
video_bitrate = int(video_bitrate * bitrate_ratio * 1.05)
scale += 50
else:
print('Reached maximum number of attempts without achieving target size.')
# Clean up two-pass log files generated by ffmpeg
for f in ['ffmpeg2pass-0.log', 'ffmpeg2pass-0.log.mbtree']:
if os.path.exists(f):
os.remove(f)
def main():
parser = argparse.ArgumentParser(
description='Compress a video file to a target size while maintaining quality.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument('input_file', type=Path, help='Path to the input video file')
parser.add_argument('output_file', type=Path, help='Path to save the compressed video file')
parser.add_argument('-s', '--target-size', type=float, default=5, help='Target file size in megabytes')
parser.add_argument('-b', '--initial-bitrate', type=int, default=500, help='Initial video bitrate in kbps')
parser.add_argument('-w', '--initial-scale', type=int, default=640, help='Initial video width in pixels')
parser.add_argument('--min-bitrate', type=int, default=50, help='Minimum allowed video bitrate in kbps')
parser.add_argument('--min-scale', type=int, default=100, help='Minimum allowed video width in pixels')
parser.add_argument('-a', '--audio-bitrate', type=int, default=96, help='Audio bitrate in kbps')
parser.add_argument('-m', '--max-attempts', type=int, default=5, help='Maximum number of attempts')
args = parser.parse_args()
# Check if input file exists
if not args.input_file.exists():
parser.error(f'Input file does not exist: {args.input_file}')
# Create output directory if it doesn't exist
args.output_file.parent.mkdir(parents=True, exist_ok=True)
try:
compress_video(
args.input_file,
args.output_file,
args.target_size,
args.initial_bitrate,
args.initial_scale,
args.min_bitrate,
args.min_scale,
args.audio_bitrate,
args.max_attempts
)
except Exception as e:
parser.error(f'Error during compression: {str(e)}')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment