[postprocessor:ugoira] use 'image2' demuxer (#1550)

Ensures exact frame timecodes with no duplicate frames.

Possible issues are the duration the last frame in an Ugoira with variable
frame durations is shown and insufficient timestamp precision of the
underlying file system (e.g. FAT32, ext3; works on ext4, tmpfs, NTFS).
pull/1633/head
Mike Fährmann 3 years ago
parent b8de0671e5
commit be9547a5f3
No known key found for this signature in database
GPG Key ID: 5680CA389D365A88

@ -2498,6 +2498,18 @@ Description
Additional FFmpeg command-line arguments. Additional FFmpeg command-line arguments.
ugoira.ffmpeg-demuxer
---------------------
Type
``string``
Default
``image2``
Description
FFmpeg demuxer to read input files with. Possible values are
"`image2 <https://ffmpeg.org/ffmpeg-formats.html#image2-1>`_" and
"`concat <https://ffmpeg.org/ffmpeg-formats.html#concat-1>`_".
ugoira.ffmpeg-location ugoira.ffmpeg-location
---------------------- ----------------------
Type Type

@ -34,6 +34,11 @@ class UgoiraPP(PostProcessor):
if rate != "auto": if rate != "auto":
self.calculate_framerate = lambda _: (None, rate) self.calculate_framerate = lambda _: (None, rate)
if options.get("ffmpeg-demuxer") == "concat":
self._process = self._concat
else:
self._process = self._image2
if options.get("libx264-prevent-odd", True): if options.get("libx264-prevent-odd", True):
# get last video-codec argument # get last video-codec argument
vcodec = None vcodec = None
@ -72,38 +77,17 @@ class UgoiraPP(PostProcessor):
if not self._frames: if not self._frames:
return return
rate_in, rate_out = self.calculate_framerate(self._frames)
with tempfile.TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory() as tempdir:
# extract frames # extract frames
try: try:
with zipfile.ZipFile(pathfmt.temppath) as zfile: with zipfile.ZipFile(pathfmt.temppath) as zfile:
zfile.extractall(tempdir) zfile.extractall(tempdir)
except FileNotFoundError: except FileNotFoundError:
pathfmt.temppath = pathfmt.realpath pathfmt.realpath = pathfmt.temppath
return return
# write ffconcat file # process frames and collect command-line arguments
ffconcat = tempdir + "/ffconcat.txt" args = self._process(tempdir)
with open(ffconcat, "w") as file:
file.write("ffconcat version 1.0\n")
for frame in self._frames:
file.write("file '{}'\n".format(frame["file"]))
file.write("duration {}\n".format(frame["delay"] / 1000))
if self.extension != "gif":
# repeat the last frame to prevent it from only being
# displayed for a very short amount of time
file.write("file '{}'\n".format(self._frames[-1]["file"]))
# collect command-line arguments
args = [self.ffmpeg]
if rate_in:
args += ("-r", str(rate_in))
args += ("-i", ffconcat)
if rate_out:
args += ("-r", str(rate_out))
if self.prevent_odd:
args += ("-vf", "crop=iw-mod(iw\\,2):ih-mod(ih\\,2)")
if self.args: if self.args:
args += self.args args += self.args
self.log.debug("ffmpeg args: %s", args) self.log.debug("ffmpeg args: %s", args)
@ -112,7 +96,7 @@ class UgoiraPP(PostProcessor):
pathfmt.set_extension(self.extension) pathfmt.set_extension(self.extension)
try: try:
if self.twopass: if self.twopass:
if "-f" not in args: if "-f" not in self.args:
args += ("-f", self.extension) args += ("-f", self.extension)
args += ("-passlogfile", tempdir + "/ffmpeg2pass", "-pass") args += ("-passlogfile", tempdir + "/ffmpeg2pass", "-pass")
self._exec(args + ["1", "-y", os.devnull]) self._exec(args + ["1", "-y", os.devnull])
@ -131,6 +115,45 @@ class UgoiraPP(PostProcessor):
else: else:
pathfmt.set_extension("zip") pathfmt.set_extension("zip")
def _concat(self, path):
# write ffconcat file
ffconcat = path + "/ffconcat.txt"
with open(ffconcat, "w") as file:
file.write("ffconcat version 1.0\n")
for frame in self._frames:
file.write("file '{}'\n".format(frame["file"]))
file.write("duration {}\n".format(frame["delay"] / 1000))
if self.extension != "gif":
# repeat the last frame to prevent it from only being
# displayed for a very short amount of time
file.write("file '{}'\n".format(frame["file"]))
rate_in, rate_out = self.calculate_framerate(self._frames)
args = [self.ffmpeg, "-f", "concat"]
if rate_in:
args += ("-r", str(rate_in))
args += ("-i", ffconcat)
if rate_out:
args += ("-r", str(rate_out))
return args
def _image2(self, path):
path += "/"
# adjust frame mtime values
ts = 0
for frame in self._frames:
os.utime(path + frame["file"], ns=(ts, ts))
ts += frame["delay"] * 1000000
return [
self.ffmpeg,
"-f", "image2",
"-ts_from_file", "2",
"-pattern_type", "sequence",
"-i", path.replace("%", "%%") + "%06d.jpg",
]
def _exec(self, args): def _exec(self, args):
out = None if self.output else subprocess.DEVNULL out = None if self.output else subprocess.DEVNULL
return subprocess.Popen(args, stdout=out, stderr=out).wait() return subprocess.Popen(args, stdout=out, stderr=out).wait()

Loading…
Cancel
Save