From be9547a5f31681bcc9063fcffef1c4b96b3e5322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20F=C3=A4hrmann?= Date: Wed, 26 May 2021 02:04:21 +0200 Subject: [PATCH] [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). --- docs/configuration.rst | 12 +++++ gallery_dl/postprocessor/ugoira.py | 73 ++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index fbd1da50..8006d989 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -2498,6 +2498,18 @@ Description Additional FFmpeg command-line arguments. +ugoira.ffmpeg-demuxer +--------------------- +Type + ``string`` +Default + ``image2`` +Description + FFmpeg demuxer to read input files with. Possible values are + "`image2 `_" and + "`concat `_". + + ugoira.ffmpeg-location ---------------------- Type diff --git a/gallery_dl/postprocessor/ugoira.py b/gallery_dl/postprocessor/ugoira.py index e8a2c591..86d0f620 100644 --- a/gallery_dl/postprocessor/ugoira.py +++ b/gallery_dl/postprocessor/ugoira.py @@ -34,6 +34,11 @@ class UgoiraPP(PostProcessor): if rate != "auto": 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): # get last video-codec argument vcodec = None @@ -72,38 +77,17 @@ class UgoiraPP(PostProcessor): if not self._frames: return - rate_in, rate_out = self.calculate_framerate(self._frames) - with tempfile.TemporaryDirectory() as tempdir: # extract frames try: with zipfile.ZipFile(pathfmt.temppath) as zfile: zfile.extractall(tempdir) except FileNotFoundError: - pathfmt.temppath = pathfmt.realpath + pathfmt.realpath = pathfmt.temppath return - # write ffconcat file - ffconcat = tempdir + "/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(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)") + # process frames and collect command-line arguments + args = self._process(tempdir) if self.args: args += self.args self.log.debug("ffmpeg args: %s", args) @@ -112,7 +96,7 @@ class UgoiraPP(PostProcessor): pathfmt.set_extension(self.extension) try: if self.twopass: - if "-f" not in args: + if "-f" not in self.args: args += ("-f", self.extension) args += ("-passlogfile", tempdir + "/ffmpeg2pass", "-pass") self._exec(args + ["1", "-y", os.devnull]) @@ -131,6 +115,45 @@ class UgoiraPP(PostProcessor): else: 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): out = None if self.output else subprocess.DEVNULL return subprocess.Popen(args, stdout=out, stderr=out).wait()