Created
March 21, 2022 15:15
-
-
Save jerrylususu/2442f9a58020c24af3f1461cf907f140 to your computer and use it in GitHub Desktop.
Key-frame based FFmpeg Cut
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
# WARNING: NOT QUITE WORKING... | |
import sys | |
import subprocess | |
import bisect | |
from pathlib import Path | |
def run_command(command_list): | |
success = True | |
stdout = b"" | |
print("executing: ", " ".join(command_list)) | |
try: | |
stdout = subprocess.check_output(command_list) | |
except subprocess.CalledProcessError: | |
success = False | |
return stdout.decode(), success | |
# https://stackoverflow.com/questions/63548027/cut-a-video-in-between-key-frames-without-re-encoding-the-full-video-using-ffpme | |
def get_keyframe_list(input_file): | |
probe_command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-skip_frame", "nokey", "-show_entries", "frame=pkt_pts_time", "-of", "csv=p=0", input_file] | |
output, _ = run_command(probe_command) | |
keyframes = [float(line) for line in output.split("\n") if line] | |
return keyframes | |
def find_closest_range(keyframe_list, start_sec, end_sec): | |
if len(keyframe_list) < 2: | |
raise Exception("too few keyframe...") | |
# some binary search magic that seems to work? | |
start_index = bisect.bisect_left(keyframe_list, start_sec) | |
end_index = bisect.bisect(keyframe_list, end_sec)-1 | |
return keyframe_list[start_index], keyframe_list[end_index] | |
def time_to_sec(time_str): | |
ms = 0 | |
if ":" not in time_str: | |
raise Exception("invalid time format") | |
if "." in time_str: | |
time_str, ms = time_str.split(".") | |
ms = float("0." + ms) | |
times = time_str.split(":") | |
hour, min, sec = 0, int(times[-2]), int(times[-1]) | |
if len(times) == 3: | |
hour = int(times[0]) | |
precise_second = hour*60*60 + min*60 + sec + ms | |
return precise_second | |
def ffmpeg_encode_cut(in_file, start, end, out_file): | |
# EDIT ENCODE PARAMETER HERE! | |
cut_command = ["ffmpeg", "-i", in_file,"-ss", str(start), "-to", str(end), "-c:v", "libx264", "-crf", "18", "-y", out_file] | |
# print(" ".join(cut_command)) | |
output, success = run_command(cut_command) | |
return success | |
def ffmpeg_stream_copy_cut(in_file, start, end, out_file): | |
cut_command = ["ffmpeg", "-ss", str(start), "-i", in_file, "-to", str(end), "-c:v", "copy", "-c:a", "copy", "-y", out_file] | |
output, success = run_command(cut_command) | |
return success | |
def ffmpeg_concat(segments, out_file): | |
segments = [segment for segment in segments if Path(segment).exists()] | |
with open("concat.txt", "w") as f: | |
for segment in segments: | |
f.write("file '{}'\n".format(segment)) | |
concat_command = ["ffmpeg", "-f", "concat", "-safe", "0", "-i", "concat.txt", "-c", "copy", "-y", out_file] | |
output, success = run_command(concat_command) | |
return success | |
def do_cut(file, start, end): | |
keyframes = get_keyframe_list(file) | |
start_sec, end_sec = time_to_sec(start), time_to_sec(end) | |
start_frame, end_frame = find_closest_range(keyframes, start_sec, end_sec) | |
print(f"{start_sec=} {start_frame=} {end_frame=} {end_sec=}") | |
ffmpeg_encode_cut(file, start_sec, start_frame, "before.mp4") | |
ffmpeg_stream_copy_cut(file, start_frame, end_frame, "inside.mp4") | |
ffmpeg_encode_cut(file, end_frame, end_sec, "after.mp4") | |
ffmpeg_concat(["before.mp4", "inside.mp4", "after.mp4"], "output.mp4") | |
def main(): | |
print("hello world") | |
if len(sys.argv) < 4: | |
print("usage: cut.py [file] [start] [end]") | |
print("example: cut.py input.mp4 00:01:02.02 00:01:04.88") | |
print("time format: (hh:)mm:ss(.ms)") | |
sys.exit(1) | |
do_cut(sys.argv[1], sys.argv[2], sys.argv[3]) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment