120 lines
7.8 KiB
Python
120 lines
7.8 KiB
Python
from typing import List, Optional
|
|
|
|
import subprocess
|
|
|
|
from .vision import pack_resolution, restrict_video_fps
|
|
|
|
from .typing import Fps, FrameFormat, OutputVideoEncoder, OutputVideoPreset, Resolution
|
|
from .filesystem import get_temp_frames_pattern
|
|
|
|
def run_ffmpeg(args : List[str]):
|
|
commands = [ 'ffmpeg', '-hide_banner', '-loglevel', 'error' ]
|
|
commands.extend(args)
|
|
process = subprocess.Popen(commands, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
|
|
# TODO Make timeout be a option?
|
|
code = process.wait(timeout = 300)
|
|
|
|
if code != 0:
|
|
msg = f"code: {code}"
|
|
stderr = "stderr: "
|
|
stdout = "stdout: "
|
|
if process.stderr is not None:
|
|
stderr += process.stderr.readline().decode('utf-8')
|
|
if process.stdout is not None:
|
|
stdout += process.stdout.readline().decode('utf-8')
|
|
print(', '.join([msg, stderr, stdout]))
|
|
return code == 0
|
|
|
|
def extract_frames(video_path: str, frames_path: str, video_resolution : Resolution, video_fps : Fps, trim_frame_start : Optional[int] = None, trim_frame_end: Optional[int] = None, frame_format: FrameFormat = 'png') -> bool:
|
|
# TODO Get frame image format from options.
|
|
temp_frames_pattern = get_temp_frames_pattern(frames_path, '%04d', frame_format)
|
|
commands = [ '-hwaccel', 'auto', '-i', video_path, '-q:v', '0' ]
|
|
|
|
format_resolution = pack_resolution(video_resolution)
|
|
|
|
if trim_frame_start is not None and trim_frame_end is not None:
|
|
commands.extend([ '-vf', 'trim=start_frame=' + str(trim_frame_start) + ':end_frame=' + str(trim_frame_end) + ',scale=' + format_resolution + ',fps=' + str(video_fps) ])
|
|
elif trim_frame_start is not None:
|
|
commands.extend([ '-vf', 'trim=start_frame=' + str(trim_frame_start) + ',scale=' + format_resolution + ',fps=' + str(video_fps) ])
|
|
elif trim_frame_end is not None:
|
|
commands.extend([ '-vf', 'trim=end_frame=' + str(trim_frame_end) + ',scale=' + format_resolution + ',fps=' + str(video_fps) ])
|
|
else:
|
|
commands.extend([ '-vf', 'scale=' + format_resolution + ',fps=' + str(video_fps) ])
|
|
commands.extend([ '-vsync', '0', temp_frames_pattern ])
|
|
return run_ffmpeg(commands)
|
|
|
|
def merge_frames(video_path: str, frames_dir: str, output_path: str, video_resolution: Resolution, video_fps: Fps, output_video_encoder: OutputVideoEncoder = 'libx264', output_video_quality: int = 80, output_video_preset: OutputVideoPreset = 'veryfast', frame_format: FrameFormat = 'png') -> bool:
|
|
temp_video_fps = restrict_video_fps(video_path, video_fps)
|
|
temp_frames_pattern = get_temp_frames_pattern(frames_dir, '%04d', frame_format)
|
|
commands = [ '-hwaccel', 'auto', '-s', pack_resolution(video_resolution), '-r', str(temp_video_fps), '-i', temp_frames_pattern, '-c:v', output_video_encoder ]
|
|
|
|
if output_video_encoder in [ 'libx264', 'libx265' ]:
|
|
output_video_compression = round(51 - (output_video_quality * 0.51))
|
|
commands.extend([ '-crf', str(output_video_compression), '-preset', output_video_preset ])
|
|
if output_video_encoder in [ 'libvpx-vp9' ]:
|
|
output_video_compression = round(63 - (output_video_quality * 0.63))
|
|
commands.extend([ '-crf', str(output_video_compression) ])
|
|
if output_video_encoder in [ 'h264_nvenc', 'hevc_nvenc' ]:
|
|
output_video_compression = round(51 - (output_video_quality * 0.51))
|
|
commands.extend([ '-cq', str(output_video_compression), '-preset', output_video_preset ])
|
|
if output_video_encoder in [ 'h264_amf', 'hevc_amf' ]:
|
|
output_video_compression = round(51 - (output_video_quality * 0.51))
|
|
commands.extend([ '-qp_i', str(output_video_compression), '-qp_p', str(output_video_compression), '-quality', map_amf_preset(output_video_preset) ])
|
|
commands.extend([ '-vf', 'format=rgba,framerate=fps=' + str(video_fps), '-pix_fmt', 'yuv420p', '-colorspace', 'bt709', '-y', output_path ])
|
|
return run_ffmpeg(commands)
|
|
|
|
# ffmpeg -i ~/Downloads/temp/bg.mp4 -framerate 30 -i %04d.png -filter_complex "[0:v]scale=528:960:force_original_aspect_ratio=increase,crop=528:960[bg];[1:v]format=rgba[overlay];[bg][overlay]overlay=shortest=1" -pix_fmt yuv420p -c:a copy output_final.mp4
|
|
def merge_frames_and_bg_video(video_path: str, bg_video_path: str, frames_dir: str, output_path: str, video_resolution: Resolution, video_fps: Fps, output_video_encoder: OutputVideoEncoder = 'libx264', output_video_quality: int = 80, output_video_preset: OutputVideoPreset = 'veryfast', frame_format: FrameFormat = 'png'):
|
|
temp_video_fps = restrict_video_fps(video_path, video_fps)
|
|
temp_frames_pattern = get_temp_frames_pattern(frames_dir, '%04d', frame_format)
|
|
video_resolution[0]
|
|
commands = [ '-hwaccel', 'auto', '-r', str(temp_video_fps), '-i', bg_video_path, '-i', temp_frames_pattern, '-c:v', output_video_encoder ]
|
|
|
|
width = video_resolution[0]
|
|
height = video_resolution[1]
|
|
commands.extend(["-filter_complex", f"[0:v]scale={width}:{height}:force_original_aspect_ratio=increase,crop={width}:{height}[bg];[1:v]format=rgba[overlay];[bg][overlay]overlay=shortest=1"])
|
|
|
|
if output_video_encoder in [ 'libx264', 'libx265' ]:
|
|
output_video_compression = round(51 - (output_video_quality * 0.51))
|
|
commands.extend([ '-crf', str(output_video_compression), '-preset', output_video_preset ])
|
|
if output_video_encoder in [ 'libvpx-vp9' ]:
|
|
output_video_compression = round(63 - (output_video_quality * 0.63))
|
|
commands.extend([ '-crf', str(output_video_compression) ])
|
|
if output_video_encoder in [ 'h264_nvenc', 'hevc_nvenc' ]:
|
|
output_video_compression = round(51 - (output_video_quality * 0.51))
|
|
commands.extend([ '-cq', str(output_video_compression), '-preset', output_video_preset ])
|
|
if output_video_encoder in [ 'h264_amf', 'hevc_amf' ]:
|
|
output_video_compression = round(51 - (output_video_quality * 0.51))
|
|
commands.extend([ '-qp_i', str(output_video_compression), '-qp_p', str(output_video_compression), '-quality', map_amf_preset(output_video_preset) ])
|
|
commands.extend(['-pix_fmt', 'yuv420p', '-colorspace', 'bt709', '-y', output_path])
|
|
return run_ffmpeg(commands)
|
|
|
|
# ffmpeg -i bg.mp4 -i fg.mov -filter_complex "[0:v]scale=528:960:force_original_aspect_ratio=increase,crop=528:960[bg];[1:v]scale=528:960:force_original_aspect_ratio=increase,crop=528:960[fg];[bg][fg]overlay=shortest=1" -c:a copy output_final.mp4
|
|
def merge_videos(fg_video_path: str, bg_video_path: str, output_path: str, resolution: Resolution):
|
|
width = resolution[0]
|
|
height = resolution[1]
|
|
filter_complex = f"[0:v]scale={width}:{height}:force_original_aspect_ratio=increase,crop={width}:{height}[bg];[1:v]scale={width}:{height}:force_original_aspect_ratio=increase,crop={width}:{height}[fg];[bg][fg]overlay=shortest=1"
|
|
commands = ["-i", bg_video_path, "-i", fg_video_path, "-filter_complex", filter_complex, "-c:a", "copy", "-y", output_path]
|
|
return run_ffmpeg(commands)
|
|
|
|
def restore_audio(video_path: str, audio_path: str, output_path: str, output_video_fps: Fps, trim_frame_start: Optional[int] = None, trim_frame_end: Optional[int] = None) -> bool:
|
|
commands = [ '-hwaccel', 'auto', '-i', video_path ]
|
|
|
|
if trim_frame_start is not None:
|
|
start_time = trim_frame_start / output_video_fps
|
|
commands.extend([ '-ss', str(start_time) ])
|
|
if trim_frame_end is not None:
|
|
end_time = trim_frame_end / output_video_fps
|
|
commands.extend([ '-to', str(end_time) ])
|
|
commands.extend([ '-i', audio_path, '-c', 'copy', '-map', '0:v:0', '-map', '1:a?:0', '-shortest', '-y', output_path ])
|
|
return run_ffmpeg(commands)
|
|
|
|
def map_amf_preset(output_video_preset : OutputVideoPreset) -> str:
|
|
if output_video_preset in [ 'ultrafast', 'superfast', 'veryfast' ]:
|
|
return 'speed'
|
|
if output_video_preset in [ 'faster', 'fast', 'medium' ]:
|
|
return 'balanced'
|
|
if output_video_preset in [ 'slow', 'slower', 'veryslow' ]:
|
|
return 'quality'
|
|
return 'balanced'
|