Created
October 28, 2024 19:47
-
-
Save twobob/ba30a21e763a255d5b8dc0643ab537b6 to your computer and use it in GitHub Desktop.
aim to hit a size without going over for mp4 files
This file contains 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
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