You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
6.6 KiB
Python
201 lines
6.6 KiB
Python
import ffmpeg
|
|
import subprocess
|
|
import json
|
|
from collections import Counter
|
|
import shutil
|
|
import os
|
|
|
|
def is_av1(filepath):
|
|
"""Check if a video file is already AV1-encoded."""
|
|
try:
|
|
probe = ffmpeg.probe(filepath)
|
|
for stream in probe['streams']:
|
|
if stream['codec_type'] == 'video' and 'codec_name' in stream:
|
|
if stream['codec_name'] == 'av1':
|
|
return True
|
|
except ffmpeg.Error as e:
|
|
print(f"Error probing {filepath}: {e}")
|
|
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']
|
|
video_stream = next(
|
|
(stream for stream in probe['streams'] if stream['codec_type'] == 'video'),
|
|
None
|
|
)
|
|
if video_stream:
|
|
# Convert from bits/sec to kbps
|
|
bitrate_kbps = int(format_info['bit_rate']) // 1000
|
|
width = video_stream['width']
|
|
height = video_stream['height']
|
|
return bitrate_kbps, (width, height)
|
|
except ffmpeg.Error as e:
|
|
print(f"Error getting video info for {filepath}: {e}")
|
|
return None, (None, None)
|
|
|
|
def get_fps(filepath):
|
|
"""Get the frames per second (FPS) of the input video using ffmpeg.probe."""
|
|
try:
|
|
probe = ffmpeg.probe(filepath)
|
|
video_stream = next((s for s in probe['streams'] if s['codec_type'] == 'video'), None)
|
|
if video_stream and 'r_frame_rate' in video_stream:
|
|
fps_str = video_stream['r_frame_rate'] # e.g. "30/1", "25/1"
|
|
num, den = map(int, fps_str.split('/'))
|
|
return num / den
|
|
except ffmpeg.Error as e:
|
|
print(f"Error getting FPS for {filepath}: {e}")
|
|
return None
|
|
|
|
def get_video_metadata(video_path):
|
|
"""Minimal example to get width/height from FFprobe directly via subprocess."""
|
|
cmd = [
|
|
"ffprobe", "-v", "error", "-select_streams", "v:0",
|
|
"-show_entries", "stream=width,height,bit_rate",
|
|
"-of", "json", video_path
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
return {"width": 0, "height": 0, "bit_rate": 0}
|
|
|
|
try:
|
|
data = json.loads(result.stdout)
|
|
streams = data.get("streams", [])
|
|
if not streams:
|
|
return {"width": 0, "height": 0, "bit_rate": 0}
|
|
stream = streams[0]
|
|
width = int(stream.get("width", 0))
|
|
height = int(stream.get("height", 0))
|
|
br = int(stream.get("bit_rate", 0)) # in bits per second
|
|
return {"width": width, "height": height, "bit_rate": br}
|
|
except json.JSONDecodeError:
|
|
return {"width": 0, "height": 0, "bit_rate": 0}
|
|
|
|
def get_target_resolution(group):
|
|
"""Collect the most common resolution from the group's videos."""
|
|
resolutions = []
|
|
for v in group:
|
|
meta = get_video_metadata(v["filepath"])
|
|
width, height = meta["width"], meta["height"]
|
|
if width > 0 and height > 0:
|
|
resolutions.append((width, height))
|
|
|
|
if not resolutions:
|
|
return (1280, 720)
|
|
|
|
counter = Counter(resolutions)
|
|
return counter.most_common(1)[0][0] # (width, height)
|
|
|
|
def get_target_bitrate(width, height):
|
|
"""Your existing function to choose a bitrate based on resolution."""
|
|
resolutions = {(854, 480): 1000,(1280, 720): 1500,(1920, 1080): 3000,(2560, 1440): 5000,(3840, 2160): 12000}
|
|
for res, bitrate in resolutions.items():
|
|
if width <= res[0] and height <= res[1]:
|
|
return bitrate
|
|
return 2500
|
|
|
|
def concatenate_videos(videos_list, reencode_concate = False):
|
|
"""
|
|
Concatenate pre-grouped videos, then re-encode them using AV1 (NVENC)
|
|
while forcing a unified resolution and frame rate on each input
|
|
before final concatenation in one ffmpeg command.
|
|
"""
|
|
|
|
if len(videos_list) <= 1:
|
|
return False
|
|
|
|
from concat_helper import copy_concatenate_videos
|
|
copy_concat = copy_concatenate_videos(videos_list)
|
|
|
|
if copy_concat:
|
|
return copy_concat
|
|
|
|
if not reencode_concate:
|
|
return False
|
|
|
|
main_video = videos_list[0]
|
|
video_path = main_video["filepath"]
|
|
|
|
os.makedirs("temp", exist_ok=True)
|
|
os.makedirs("concated", exist_ok=True)
|
|
|
|
temp_path = os.path.join("temp", os.path.basename(video_path))
|
|
output_path = os.path.join("concated", os.path.basename(video_path))
|
|
|
|
current_bitrate, (width, height) = get_video_info(videos_list[0]['filepath'])
|
|
|
|
target_width, target_height = get_target_resolution(videos_list)
|
|
target_bitrate_kbps = get_target_bitrate(target_width, target_height)
|
|
|
|
if target_bitrate_kbps > current_bitrate:
|
|
target_bitrate_kbps = current_bitrate
|
|
|
|
max_bitrate_kbps = int(1.5 * target_bitrate_kbps)
|
|
|
|
fps_float = get_fps(video_path)
|
|
if fps_float is None or fps_float <= 0:
|
|
print(f"Could not determine FPS for {video_path}. Using default keyframe interval of 30.")
|
|
fps_float = 30.0
|
|
keyframe_interval = int(fps_float)
|
|
|
|
print(f"Concatenating {len(videos_list)} videos into {temp_path}")
|
|
print(f" Mode Resolution: {target_width}x{target_height}")
|
|
print(f" Target Bitrate: {target_bitrate_kbps}k (max ~{max_bitrate_kbps}k)")
|
|
print(f" Keyframe Interval: {keyframe_interval}")
|
|
|
|
cmd = ["ffmpeg", "-y"] # Overwrite output if exists
|
|
for v in videos_list:
|
|
cmd.extend(["-i", v["filepath"]])
|
|
|
|
filter_statements = []
|
|
concat_streams = []
|
|
n = len(videos_list)
|
|
|
|
unified_fps = 30
|
|
|
|
for i in range(n):
|
|
filter_statements.append(
|
|
f"[{i}:v]fps={unified_fps},scale={target_width}:{target_height}[v{i}]"
|
|
)
|
|
concat_streams.append(f"[v{i}][{i}:a]")
|
|
|
|
# Example final: [v0][0:a][v1][1:a]concat=n=2:v=1:a=1[outv][outa]
|
|
concat_line = "".join(concat_streams) + f"concat=n={n}:v=1:a=1[outv][outa]"
|
|
filter_statements.append(concat_line)
|
|
|
|
filter_complex = ";".join(filter_statements)
|
|
|
|
cmd.extend([
|
|
"-filter_complex", filter_complex,
|
|
"-map", "[outv]",
|
|
"-map", "[outa]",
|
|
"-c:v", "av1_nvenc",
|
|
"-b:v", f"{target_bitrate_kbps}k",
|
|
"-maxrate", f"{max_bitrate_kbps}k",
|
|
"-bufsize", f"{max_bitrate_kbps}k",
|
|
"-preset", "p5",
|
|
"-g", str(keyframe_interval),
|
|
"-c:a", "aac",
|
|
"-b:a", "192k",
|
|
temp_path
|
|
])
|
|
|
|
try:
|
|
subprocess.run(cmd, check=True)
|
|
except:
|
|
return False
|
|
|
|
for video in videos_list:
|
|
os.remove(video["filepath"])
|
|
|
|
shutil.move(temp_path, output_path)
|
|
|
|
return main_video
|
|
|
|
def get_file_size_in_mb(file_path):
|
|
return os.path.getsize(file_path) / (1024 * 1024) |