From ca12d13a40a8c3b03117c60a211828064e989bb2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 17 Apr 2025 12:50:28 +0200 Subject: [PATCH] Improves thumbnail extraction reliability Enhances thumbnail extraction by retrying without seeking if the initial attempt fails. This addresses issues where the generated thumbnail file is either missing or empty. It also calculates the seek position more accurately and avoid seeking for very short videos. --- .../plugins/publish/extract_thumbnail.py | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index b72862ea22..89bb9a90ab 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -486,25 +486,37 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # Set video input attributes max_int = str(2147483647) video_data = get_ffprobe_data(video_file_path, logger=self.log) - # Use duration of the individual streams since it is returned with - # higher decimal precision than 'format.duration'. We need this - # more precise value for calculating the correct amount of frames - # for higher FPS ranges or decimal ranges, e.g. 29.97 FPS - duration = max( - float(stream.get("duration", 0)) - for stream in video_data["streams"] - if stream.get("codec_type") == "video" - ) + # Get duration or use a safe default (single frame) + duration = 0 + for stream in video_data["streams"]: + if stream.get("codec_type") == "video": + stream_duration = float(stream.get("duration", 0)) + if stream_duration > duration: + duration = stream_duration + + # For very short videos, just use the first frame + # Calculate seek position safely + seek_position = 0 + if duration > 0.1: # Only use timestamp calculation for videos longer than 0.1 seconds + seek_position = duration * self.duration_split + + # Build command args cmd_args = [ "-y", - "-ss", str(duration * self.duration_split), "-i", video_file_path, "-analyzeduration", max_int, "-probesize", max_int, - "-frames:v", "1" ] + # Only add -ss if we're seeking to a specific position + if seek_position > 0: + cmd_args.insert(1, "-ss") + cmd_args.insert(2, str(seek_position)) + + # Ensure we extract exactly one frame + cmd_args.extend(["-frames:v", "1"]) + # add output file path cmd_args.append(output_thumb_file_path) @@ -517,9 +529,34 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # run subprocess self.log.debug("Executing: {}".format(" ".join(cmd))) run_subprocess(cmd, logger=self.log) - self.log.debug( - "Thumbnail created: {}".format(output_thumb_file_path)) - return output_thumb_file_path + + # Verify the output file was created + if os.path.exists(output_thumb_file_path) and os.path.getsize(output_thumb_file_path) > 0: + self.log.debug( + "Thumbnail created: {}".format(output_thumb_file_path)) + return output_thumb_file_path + else: + self.log.warning( + "Output file was not created or is empty: {}".format(output_thumb_file_path)) + + # Fallback to extracting the first frame without seeking + if "-ss" in cmd_args: + self.log.debug("Trying fallback without seeking") + # Remove -ss and its value + ss_index = cmd_args.index("-ss") + cmd_args.pop(ss_index) # Remove -ss + cmd_args.pop(ss_index) # Remove the timestamp value + + # Create new command and try again + cmd = get_ffmpeg_tool_args("ffmpeg", *cmd_args) + self.log.debug("Fallback command: {}".format(" ".join(cmd))) + run_subprocess(cmd, logger=self.log) + + if os.path.exists(output_thumb_file_path) and os.path.getsize(output_thumb_file_path) > 0: + self.log.debug("Fallback thumbnail created: {}".format(output_thumb_file_path)) + return output_thumb_file_path + + return None except RuntimeError as error: self.log.warning( "Failed intermediate thumb source using ffmpeg: {}".format(