ensnare
ensnare

Reputation: 42143

Extract the middle frame of a video in Python using ffmpeg?

I have a collection of video files for which I'd like to generate thumbnails. How can I extract the middle frame of the video in Python?

If I execute:

ffmpeg -i /tmp/0.mts

I get a line returned to the shell that says:

Duration: 00:00:04.49, start: 1.016689, bitrate: 25234 kb/s

Maybe I could extract the timestamp after the duration and somehow divide that by 2 to get the middle frame timestamp, and extract that?

If I'm recursing through a large collection of videos, is there a way to keep ffmpeg "open" without the overhead of opening/closing it to identify each file?

Upvotes: 3

Views: 2796

Answers (1)

nmaier
nmaier

Reputation: 33202

As it turns out I had to solve more or less exactly the same thing a while back. One additional requirement was not to use ffprobe (as it might not be available, but ffmpeg would). ffprobe as @LordNeckbeard suggests would have been better for the task of getting the duration otherwise.

So here is some sparsely documented code that should be easy enough to understand nonetheless. If not, don't hesitate to ask.

#!/usr/bin/env python

# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
# Written in 2013 - Nils Maier

import datetime
import os
import re
import subprocess
import sys


def which(program):
    """ Somewhat equivalent to which(1) """

    def is_executable(fpath):
        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)

    if is_executable(program):
        return program
    path, program = os.path.split(program)
    if path:
        return None
    for path in os.environ["PATH"].split(os.pathsep):
        path = path.strip('"')
        exe = os.path.join(path, program)
        if is_executable(exe):
            return exe
        # Windows-style
        exe = os.path.join(path, "{}.exe".format(program))
        if is_executable(exe):
            return exe
    return None


def thumb_with_ffmpeg(infile, position=0.5, executable=None):
    """
    Extract a thumbnail using ffmpeg

    :param infile: File to thumbnail.
    :param position: Position at which to take the thumbnail. Default: 0.5
    :param executable: Executable to use. Default: first "ffmpeg" in $PATH
    :returns: The thumbnail data (binary string)
    """

    ffmpeg = which(executable or "ffmpeg")
    if not ffmpeg:
        raise RuntimeError(
            "Failed to find ffmpeg executable: {}".format(executable))
    if position < 0 or position >= 1.0:
        raise ValueError(
            "Position {} is not between 0.0 and 1.0".format(position))

    proc = subprocess.Popen([ffmpeg, "-i", infile], stderr=subprocess.PIPE)
    _, result = proc.communicate()
    m = re.search(r"Duration:\s*(\d+):(\d+):(\d+)\.(\d+)", result)
    if not m:
        raise KeyError("Cannot determine duration")
    # Avoiding strptime here because it has some issues handling milliseconds.
    m = [int(m.group(i)) for i in range(1, 5)]
    duration = datetime.timedelta(hours=m[0],
                                  minutes=m[1],
                                  seconds=m[2],
                                  # * 10 because truncated to 2 decimal places
                                  milliseconds=m[3] * 10
                                  ).total_seconds()
    target = max(0, min(duration * position, duration - 0.1))
    target = "{:.3f}".format(target)
    args = [ffmpeg,
            "-ss", target,
            "-i", infile,
            "-map", "v:0",     # first video stream
            "-frames:v", "1",  # 1 frame
            "-f", "mjpeg",     # motion jpeg (aka. jpeg since 1 frame) output
            "pipe:"            # pipe output to stdout
            ]
    proc = subprocess.Popen(args, stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE)
    output, _ = proc.communicate()
    if proc.returncode:
        raise subprocess.CalledProcessError(proc.returncode, args)
    if not output:
        raise subprocess.CalledProcessError(-2, args)
    return output


if __name__ == "__main__":
    from argparse import ArgumentParser, ArgumentTypeError

    def percentage(x):
        x = float(x)
        if x < 0.0 or x >= 1.0:
            raise ArgumentTypeError(
                "{} not in percentage range [0.0, 1.0)".format(x))
        return x

    parser = ArgumentParser(
        description="Extract a thumbnail from a media file using ffmpeg")
    parser.add_argument("infile", type=str, help="Input file")
    parser.add_argument("outfile", type=str, help="Output file")
    parser.add_argument("-f", "--ffmpeg", type=str, default=None,
                        help="use this ffmpeg binary, "
                             "default: check $PATH for ffmpeg")
    parser.add_argument("-p", "--position", type=percentage, default=0.5,
                        help="thumbnail at this position (percentage), "
                             "default: 0.5")
    args = parser.parse_args()

    try:
        output = thumb_with_ffmpeg(args.infile, args.position, args.ffmpeg)
        with open(args.outfile, "wb") as op:
            op.write(output)
    except Exception as ex:
        print >>sys.stderr, "Error:", ex
        sys.exit(ex.returncode or 1)

Upvotes: 2

Related Questions