Editing videos using the ffmpeg and ffprobe module in python

I am working on an ETL process, and I’m now in the final stage of preprocessing my videos. I used the script below (reference: @FarisHijazi) to first auto detected black-screen frames using ffprobe and trim them out using ffmpeg.

The script worked for me but the problems are:

  1. It cut off all other good frames together with the first bad frames. e.g. if gBgBgBgB represents a sequence of good and BAD frames for 5sec each, the script only returned the first g(5sec) and cut off the other BgBgBgB after it. I want to have only g g g g where all B B B B has been removed
  2. I also want to detect other colors aside black-screen e.g. green-screen or red-screen or blurry part of video
  3. Script doesn’t work if video has no audio in it.
"""
@author: 
Use ffprobe to extract black frames and ffmpeg to trim them and output a new video
"""

import argparse
import os
import shlex
import subprocess

parser = argparse.ArgumentParser(
    __doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("input", type=str, help="input video file")
parser.add_argument(
    "--invert",
    action="store_true",
    help="remove nonblack instead of removing black",
)
args = parser.parse_args()

##FIXME: sadly you must chdir so that the ffprobe command will work
os.chdir(os.path.split(args.input)[0])
args.input = os.path.split(args.input)[1]

spl = args.input.split(".")
outpath = (
    ".".join(spl[:-1])
    + "."
    + ("invert" if args.invert else "")
    + "out."
    + spl[-1]
)


def delete_back2back(l):
    from itertools import groupby

    return [x[0] for x in groupby(l)]


def construct_ffmpeg_trim_cmd(timepairs, inpath, outpath):
    cmd = f'ffmpeg -i "{inpath}" -y -r 20 -filter_complex '
    cmd += '"'
    for i, (start, end) in enumerate(timepairs):
        cmd += (
            f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS,format=yuv420p[{i}v]; "
            + f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS[{i}a]; "
        )
    for i, (start, end) in enumerate(timepairs):
        cmd += f"[{i}v][{i}a]"
    cmd += f"concat=n={len(timepairs)}:v=1:a=1[outv][outa]"
    cmd += '"'
    cmd += f' -map [outv] -map [outa] "{outpath}"'
    return cmd


def get_blackdetect(inpath, invert=False):
    ffprobe_cmd = f'ffprobe -f lavfi -i "movie={inpath},blackdetect[out0]" -show_entries tags=lavfi.black_start,lavfi.black_end -of default=nw=1 -v quiet'
    print("ffprobe_cmd:", ffprobe_cmd)
    lines = (
        subprocess.check_output(shlex.split(ffprobe_cmd))
        .decode("utf-8")
        .split("\n")
    )
    times = [
        float(x.split("=")[1].strip()) for x in delete_back2back(lines) if x
    ]
    assert len(times), "no black scene detected"

    if not invert:
        times = [0] + times[:-1]
    timepairs = [
        (times[i], times[i + 1]) for i in range(0, len(times) // 2, 2)
    ]
    return timepairs

exec

if __name__ == "__main__":
    timepairs = get_blackdetect(args.input, invert=args.invert)
    cmd = construct_ffmpeg_trim_cmd(timepairs, args.input, outpath)

    print(cmd)
    os.system(cmd)

Hello, @Fountain, Adeshina Ibrahim, and welcome to Python.

I have done some work driving ffmeg and ffprobe from Python, so I sympathise with your situation.

My suggestion is to separate the Python questions from the ffmeg and ffprobe questions. Make a batch or script file which contains calls to ffmpeg and ffprobe commands, appropriate for just one video. Figure out what parameters you need to pass to ffprobe, and what output it gives you in return. Transform that output into parameters for ffmpeg manually.

You should, for example, be able to figure out how to make a script which works for videos which have no audio, without touching Python.

Once the script file works, and it generates the video result that you want, only then try to write a Python program which generates the parameters for ffprobe, and reads output from ffprobe, and generates the parameters for ffmpeg. You might even want to stub out the Python subprocess.run() call so that you can check the parameters.

Once the Python code works, then have it call subprocess.run(). The whole combination will now be more likely to work.

Does this help? Good luck!
—Jim DeLaHunt, Vancouver, Canada

1 Like