Last active
May 27, 2025 00:21
-
-
Save FelikZ/95d2121f4f63ddbc36edb6b2d2f3c8fc to your computer and use it in GitHub Desktop.
Script to convert videos from Nvidia Instant Replay to lower bitrate
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
#!/usr/bin/env python3 | |
""" | |
HEVC Video Encoder with NVIDIA NVENC | |
Drag and drop video files onto this script for batch processing | |
""" | |
## benchmarking | |
# ffmpeg -i ".\Albion-Online.exe\output\Albion-Online.exe 2025.05.25 - 00.13.07.16.DVR.mp4" -ss 00:02:13 -t 00:00:30 -i ".\Albion-Online.exe\Albion-Online.exe 2025.05.25 - 00.13.07.16.DVR.mp4" -filter_complex "[0:v]format=yuv420p[dis],[1:v]format=yuv420p[ref],[dis][ref]libvmaf=n_threads=16" -f null - | |
import sys | |
import os | |
import subprocess | |
import json | |
import shutil | |
from pathlib import Path | |
from datetime import datetime | |
# Configuration Variables | |
OUTPUT_DIR = "output" | |
VIDEO_CODEC = "hevc_nvenc" | |
AUDIO_CODEC = "copy" | |
BITRATE = "10000k" | |
CONTAINER = "mp4" | |
PRESET = "slow" | |
TWO_PASS = True | |
def check_ffmpeg(): | |
"""Check if ffmpeg is available""" | |
if not shutil.which("ffmpeg"): | |
print("ERROR: ffmpeg not found in PATH. Please install ffmpeg.") | |
input("Press Enter to exit...") | |
sys.exit(1) | |
def check_nvenc(): | |
"""Check if NVIDIA NVENC is available""" | |
global VIDEO_CODEC | |
try: | |
cmd = [ | |
"ffmpeg", "-f", "lavfi", "-i", "testsrc=duration=1:size=320x240:rate=1", | |
"-c:v", "hevc_nvenc", "-f", "null", "-" | |
] | |
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) | |
if "Cannot load" in result.stderr: | |
print("WARNING: NVIDIA NVENC not available. Falling back to software encoding.") | |
VIDEO_CODEC = "libx265" | |
except Exception: | |
print("WARNING: Could not test NVENC availability. Proceeding with NVENC...") | |
def get_video_info(filepath): | |
"""Get video information using ffprobe""" | |
cmd = [ | |
"ffprobe", "-v", "quiet", "-print_format", "json", | |
"-show_format", "-show_streams", str(filepath) | |
] | |
try: | |
result = subprocess.run(cmd, capture_output=True, text=True, check=True) | |
data = json.loads(result.stdout) | |
video_stream = next((s for s in data["streams"] if s["codec_type"] == "video"), None) | |
if video_stream and "r_frame_rate" in video_stream: | |
fps_parts = video_stream["r_frame_rate"].split("/") | |
fps = round(float(fps_parts[0]) / float(fps_parts[1]), 3) | |
else: | |
fps = 30 | |
duration = round(float(data["format"]["duration"]), 2) | |
return {"fps": fps, "duration": duration} | |
except Exception as e: | |
print(f" Warning: Could not get video info - {e}") | |
return {"fps": 30, "duration": 0} | |
def encode_video(input_file, output_file, fps): | |
"""Encode video using ffmpeg""" | |
# https://scottstuff.net/posts/2025/03/17/benchmarking-ffmpeg-h265/#overall-results-1 | |
base_args = [ | |
"ffmpeg", "-i", str(input_file), | |
"-y", | |
"-v", "error", | |
"-hide_banner", | |
"-stats", | |
"-pix_fmt", "yuv420p10le", | |
"-g", "600", | |
"-keyint_min", "600", | |
"-c:v", VIDEO_CODEC, | |
# "-b:v", BITRATE, | |
"-cq:v", "31", | |
# TODO: remove | |
# "-ss", "00:02:13", "-t", "00:00:30", | |
"-c:a", AUDIO_CODEC, | |
"-r", str(fps), | |
"-movflags", "+faststart" | |
] | |
if VIDEO_CODEC == "hevc_nvenc": | |
base_args.extend([ | |
"-preset", "p7", | |
"-rc", "vbr", | |
"-tune", "hq", | |
"-rc-lookahead", "20", | |
"-2pass", "true", | |
"-multipass", "fullres" | |
]) | |
elif VIDEO_CODEC == "libx265": | |
base_args.extend(["-preset", PRESET, "-x265-params", "log-level=error", "-tune", "animation"]) | |
if TWO_PASS and VIDEO_CODEC != "hevc_nvenc": | |
# Two-pass encoding (software only) | |
print(" Pass 1/2...") | |
pass1_args = base_args + ["-pass", "1", "-f", "null", os.devnull] | |
subprocess.run(pass1_args, check=True) | |
print(" Pass 2/2...") | |
pass2_args = base_args + ["-pass", "2", str(output_file)] | |
subprocess.run(pass2_args, check=True) | |
# Clean up pass files | |
for log_file in Path(".").glob("ffmpeg2pass-*.log*"): | |
log_file.unlink(missing_ok=True) | |
else: | |
# Single-pass encoding but highest quality | |
single_pass_args = base_args + [str(output_file)] | |
subprocess.run(single_pass_args, check=True) | |
def preserve_timestamps(source_file, target_file): | |
"""Preserve file timestamps from source to target""" | |
source_stat = source_file.stat() | |
os.utime(target_file, (source_stat.st_atime, source_stat.st_mtime)) | |
def find_videos_by_date(directory, target_date): | |
"""Find video files in directory modified on or after target_date""" | |
video_extensions = {'.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.mpg', '.mpeg'} | |
video_files = [] | |
dir_path = Path(directory) | |
if not dir_path.exists(): | |
print(f"Directory not found: {directory}") | |
return [] | |
target_timestamp = datetime.strptime(target_date, "%Y-%m-%d").timestamp() | |
for file_path in dir_path.glob('*'): | |
if (file_path.is_file() and | |
file_path.suffix.lower() in video_extensions and | |
file_path.stat().st_mtime >= target_timestamp): | |
video_files.append(file_path) | |
return sorted(video_files) | |
def main(): | |
if len(sys.argv) != 3: | |
print("Usage: python script.py <directory> <date_YYYY-mm-dd>") | |
print("Example: python script.py C:\\Videos 2024-01-15") | |
input("Press Enter to exit...") | |
return | |
directory = sys.argv[1] | |
date_filter = sys.argv[2] | |
# Validate date format | |
try: | |
datetime.strptime(date_filter, "%Y-%m-%d") | |
except ValueError: | |
print("Invalid date format. Use YYYY-mm-dd") | |
input("Press Enter to exit...") | |
return | |
print(f"Searching for videos in: {directory}") | |
print(f"Modified on or after: {date_filter}") | |
input_files = find_videos_by_date(directory, date_filter) | |
if not input_files: | |
print("No video files found matching criteria.") | |
input("Press Enter to exit...") | |
return | |
print(f"Found {len(input_files)} video files:") | |
for video in input_files: | |
print(f" - {video.name}") | |
print() | |
# Initial checks | |
check_ffmpeg() | |
check_nvenc() | |
total_files = len(input_files) | |
processed_files = 0 | |
success_count = 0 | |
failed_files = [] | |
print(f"Starting batch processing of {total_files} files...") | |
print("Configuration:") | |
print(f" Video Codec: {VIDEO_CODEC}") | |
print(f" Audio Codec: {AUDIO_CODEC}") | |
print(f" Two-Pass: {TWO_PASS}") | |
print() | |
for input_file in input_files: | |
processed_files += 1 | |
if not input_file.exists(): | |
print(f"[{processed_files}/{total_files}] SKIP: File not found - {input_file}") | |
failed_files.append(str(input_file)) | |
continue | |
base_name = input_file.stem | |
input_dir = input_file.parent | |
output_dir_path = input_dir / OUTPUT_DIR | |
# Create output directory if it doesn't exist | |
output_dir_path.mkdir(exist_ok=True) | |
output_file = output_dir_path / f"{base_name}.{CONTAINER}" | |
print(f"[{processed_files}/{total_files}] Processing: {input_file.name}") | |
try: | |
# Get video information | |
video_info = get_video_info(input_file) | |
print(f" Duration: {video_info['duration']}s, FPS: {video_info['fps']}") | |
# Encode video | |
start_time = datetime.now() | |
encode_video(input_file, output_file, video_info['fps']) | |
end_time = datetime.now() | |
duration = (end_time - start_time).total_seconds() | |
if output_file.exists(): | |
# Preserve original file timestamps | |
preserve_timestamps(input_file, output_file) | |
output_size = output_file.stat().st_size / (1024 * 1024) | |
print(f" ✓ Completed in {duration:.1f}s - Size: {output_size:.1f} MB") | |
success_count += 1 | |
else: | |
raise Exception("Output file was not created") | |
except Exception as e: | |
print(f" ✗ Failed: {e}") | |
failed_files.append(str(input_file)) | |
print() | |
# Summary | |
print("Batch processing complete!") | |
print(f"Successfully processed: {success_count}/{total_files} files") | |
if failed_files: | |
print("Failed files:") | |
for failed in failed_files: | |
print(f" - {failed}") | |
input("Press Enter to exit...") | |
if __name__ == "__main__": | |
main() |
Author
FelikZ
commented
May 26, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment