|
|
|
|
@ -1,11 +1,8 @@
|
|
|
|
|
import os, shutil
|
|
|
|
|
import os, shutil, config
|
|
|
|
|
import ffmpeg
|
|
|
|
|
from tqdm import tqdm
|
|
|
|
|
|
|
|
|
|
def is_av1(filepath):
|
|
|
|
|
"""
|
|
|
|
|
Check if a video file is already AV1-encoded.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
probe = ffmpeg.probe(filepath)
|
|
|
|
|
for stream in probe['streams']:
|
|
|
|
|
@ -17,10 +14,6 @@ def is_av1(filepath):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def get_video_info(filepath):
|
|
|
|
|
"""
|
|
|
|
|
Returns (bitrate_in_kbps, (width, height)) for the specified video file.
|
|
|
|
|
If probing fails, returns (None, (None, None)).
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
probe = ffmpeg.probe(filepath)
|
|
|
|
|
format_info = probe['format']
|
|
|
|
|
@ -39,10 +32,6 @@ def get_video_info(filepath):
|
|
|
|
|
return None, (None, None)
|
|
|
|
|
|
|
|
|
|
def get_files(folder):
|
|
|
|
|
"""
|
|
|
|
|
Recursively gather all .mp4 files in `folder`.
|
|
|
|
|
Sort them by file size (smallest to largest) just as an example sorting.
|
|
|
|
|
"""
|
|
|
|
|
all_files = []
|
|
|
|
|
for root, _, filenames in os.walk(folder):
|
|
|
|
|
for filename in filenames:
|
|
|
|
|
@ -51,16 +40,7 @@ def get_files(folder):
|
|
|
|
|
all_files.append(os.path.join(root, filename))
|
|
|
|
|
return sorted(all_files, key=os.path.getsize)
|
|
|
|
|
|
|
|
|
|
def parse_text_for_print(text):
|
|
|
|
|
"""
|
|
|
|
|
If string is longer than 100 characters, only print the first 100 characters.
|
|
|
|
|
"""
|
|
|
|
|
return text[:100] + '...' if len(text) > 100 else text
|
|
|
|
|
|
|
|
|
|
def get_target_bitrate(width, height):
|
|
|
|
|
"""
|
|
|
|
|
Your existing function to choose a bitrate based on resolution.
|
|
|
|
|
"""
|
|
|
|
|
resolutions = {
|
|
|
|
|
(854, 480): 1000,
|
|
|
|
|
(1280, 720): 1500,
|
|
|
|
|
@ -76,7 +56,6 @@ def get_target_bitrate(width, height):
|
|
|
|
|
return 2500
|
|
|
|
|
|
|
|
|
|
def get_fps(filepath):
|
|
|
|
|
"""Get the frames per second (FPS) of the input video."""
|
|
|
|
|
try:
|
|
|
|
|
probe = ffmpeg.probe(filepath)
|
|
|
|
|
video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
|
|
|
|
|
@ -90,15 +69,11 @@ def get_fps(filepath):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def encode_video(filepath, output_path, target_bitrate):
|
|
|
|
|
"""
|
|
|
|
|
Encode video using ffmpeg with a target bitrate (in kbps).
|
|
|
|
|
Using NVIDIA AV1 hardware encoder (av1_nvenc) for RTX 40-series.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
fps = get_fps(filepath)
|
|
|
|
|
if fps is None:
|
|
|
|
|
print(f"Could not determine FPS for {filepath}, using default keyframe interval.")
|
|
|
|
|
fps = 30 # Default fallback if FPS can't be determined
|
|
|
|
|
print(f"Could not determine FPS for {filepath}.")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
keyframe_interval = int(fps) # Set the keyframe interval to match 1 second
|
|
|
|
|
|
|
|
|
|
@ -123,18 +98,12 @@ def encode_video(filepath, output_path, target_bitrate):
|
|
|
|
|
overwrite_output=True,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
filepath_print = parse_text_for_print(filepath)
|
|
|
|
|
print(f" Finished encoding {filepath_print} to AV1 at {target_bitrate} kbps "
|
|
|
|
|
print(f" Finished encoding {os.path.basename(filepath)} to AV1 at {target_bitrate} kbps "
|
|
|
|
|
f"(maxrate={max_bitrate} kbps).")
|
|
|
|
|
except ffmpeg.Error as e:
|
|
|
|
|
filepath_print = parse_text_for_print(filepath)
|
|
|
|
|
print(f" Error encoding {filepath_print} to AV1: {e}")
|
|
|
|
|
print(f" Error encoding {os.path.basename(filepath)} to AV1: {e}")
|
|
|
|
|
|
|
|
|
|
def check_and_replace_if_smaller(original_path, temp_output_path):
|
|
|
|
|
"""
|
|
|
|
|
Compare file sizes and replace the original if the new one is smaller.
|
|
|
|
|
Otherwise, delete the temporary file.
|
|
|
|
|
"""
|
|
|
|
|
if not os.path.exists(temp_output_path):
|
|
|
|
|
print(f"[ERROR] Temp file {temp_output_path} not found. Skipping replacement...")
|
|
|
|
|
return
|
|
|
|
|
@ -162,19 +131,27 @@ def check_and_replace_if_smaller(original_path, temp_output_path):
|
|
|
|
|
shutil.move(temp_output_path, original_path)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def reencode_videos_av1(input_folder):
|
|
|
|
|
"""
|
|
|
|
|
Main loop:
|
|
|
|
|
1. Gather .mp4 files
|
|
|
|
|
2. For each file, check if it's AV1 or if it needs re-encoding based on target bitrate.
|
|
|
|
|
3. Re-encode if needed.
|
|
|
|
|
4. Compare file sizes and replace if smaller.
|
|
|
|
|
"""
|
|
|
|
|
files = get_files(input_folder)
|
|
|
|
|
for input_path in tqdm(files, desc="Processing videos", unit="file"):
|
|
|
|
|
short_name = parse_text_for_print(input_path)
|
|
|
|
|
def update_codec_db(video_id, codec):
|
|
|
|
|
conn, cursor = config.get_local_db_connection()
|
|
|
|
|
cursor.execute("UPDATE videos SET codec = %s WHERE id = %s", (codec, video_id))
|
|
|
|
|
conn.commit()
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
def reencode_videos_av1():
|
|
|
|
|
# get videos
|
|
|
|
|
conn, cursor = config.get_local_db_connection()
|
|
|
|
|
cursor.execute("SELECT * FROM videos WHERE codec IS NULL AND status != 'missing' AND filepath IS NOT NULL ORDER BY size ASC;")
|
|
|
|
|
videos = cursor.fetchall()
|
|
|
|
|
|
|
|
|
|
for video in tqdm(videos, desc="Processing videos", unit="file"):
|
|
|
|
|
input_path = video['filepath']
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(input_path):
|
|
|
|
|
print(f"🚫 File not found: {input_path}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
file_size_in_mb = os.path.getsize(input_path) / (1024 * 1024)
|
|
|
|
|
print(f"\nProcessing {short_name} ({file_size_in_mb:.2f} MB)...")
|
|
|
|
|
print(f"\nProcessing {os.path.basename(input_path)} ({file_size_in_mb:.2f} MB)...")
|
|
|
|
|
|
|
|
|
|
# 2) Get current bitrate & resolution
|
|
|
|
|
current_bitrate, (width, height) = get_video_info(input_path)
|
|
|
|
|
@ -190,39 +167,21 @@ def reencode_videos_av1(input_folder):
|
|
|
|
|
|
|
|
|
|
if is_av1(input_path):
|
|
|
|
|
print("Video is already encoded in AV1. Skipping")
|
|
|
|
|
|
|
|
|
|
# move to 'encoded' folder inside the input folder
|
|
|
|
|
new_file_path = os.path.join(input_folder, "encoded", os.path.basename(input_path))
|
|
|
|
|
os.makedirs(os.path.dirname(new_file_path), exist_ok=True)
|
|
|
|
|
shutil.move(input_path, new_file_path)
|
|
|
|
|
|
|
|
|
|
update_codec_db(video['id'], 'av1')
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# 3) Re-encode
|
|
|
|
|
output_path = os.path.join('temp', os.path.basename(input_path))
|
|
|
|
|
encode_video(input_path, output_path, target_bitrate)
|
|
|
|
|
output_path = os.path.join('.temp', os.path.basename(input_path))
|
|
|
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
|
|
|
encoded = encode_video(input_path, output_path, target_bitrate)
|
|
|
|
|
|
|
|
|
|
if not encoded:
|
|
|
|
|
print("Encoding failed. Skipping.")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# 4) Compare file sizes and replace if smaller
|
|
|
|
|
check_and_replace_if_smaller(input_path, output_path)
|
|
|
|
|
update_codec_db(video['id'], 'av1')
|
|
|
|
|
|
|
|
|
|
# move to 'encoded' folder inside the input folder
|
|
|
|
|
new_file_path = os.path.join(input_folder, "encoded", os.path.basename(input_path))
|
|
|
|
|
os.makedirs(os.path.dirname(new_file_path), exist_ok=True)
|
|
|
|
|
shutil.move(input_path, new_file_path)
|
|
|
|
|
|
|
|
|
|
# ---------------------- Main Script Entry ---------------------- #
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
if len(sys.argv) > 1:
|
|
|
|
|
input_folder = sys.argv[1]
|
|
|
|
|
else:
|
|
|
|
|
input_folder = input("Enter the input folder path: ")
|
|
|
|
|
|
|
|
|
|
if not os.path.isdir(input_folder):
|
|
|
|
|
print(f"Input folder '{input_folder}' does not exist.")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
print("Re-encoding videos to AV1 (only if bitrate is above our resolution-based presets)...")
|
|
|
|
|
reencode_videos_av1(input_folder)
|
|
|
|
|
print("All done!")
|
|
|
|
|
reencode_videos_av1()
|