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(