From ca12d13a40a8c3b03117c60a211828064e989bb2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 17 Apr 2025 12:50:28 +0200 Subject: [PATCH 1/9] 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( From bf44622c057fd6d45c8a270ae24ee0f71eb89abc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 17 Apr 2025 12:55:02 +0200 Subject: [PATCH 2/9] Improves thumbnail extraction reliability Ensures thumbnail extraction falls back to the first frame if the initial attempt fails, handling potential issues with seek position calculation or output file creation. This enhances the robustness of the thumbnail creation process. --- .../plugins/publish/extract_thumbnail.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 89bb9a90ab..e5108444f7 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -498,7 +498,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # 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 + # Only use timestamp calculation for videos longer than 0.1 seconds + if duration > 0.1: seek_position = duration * self.duration_split # Build command args @@ -531,13 +532,17 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): run_subprocess(cmd, logger=self.log) # Verify the output file was created - if os.path.exists(output_thumb_file_path) and os.path.getsize(output_thumb_file_path) > 0: + 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)) + "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: @@ -549,11 +554,18 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # Create new command and try again cmd = get_ffmpeg_tool_args("ffmpeg", *cmd_args) - self.log.debug("Fallback command: {}".format(" ".join(cmd))) + 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)) + 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 From 57b808e92477042a2caef41517dbd2284669c0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 12 May 2025 13:46:54 +0200 Subject: [PATCH 3/9] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/extract_thumbnail.py | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index e5108444f7..3626c5f381 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -497,7 +497,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # For very short videos, just use the first frame # Calculate seek position safely - seek_position = 0 + seek_position = 0.0 # Only use timestamp calculation for videos longer than 0.1 seconds if duration > 0.1: seek_position = duration * self.duration_split @@ -539,36 +539,31 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): 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 + self.log.warning("Output file was not created or is empty") + # Try to create thumbnail without offset + # - skip if offset did not happen + if "-ss" not in cmd_args: return None + + self.log.debug("Trying fallback without offset") + # 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(f"Fallback thumbnail created") + return output_thumb_file_path + return None except RuntimeError as error: self.log.warning( "Failed intermediate thumb source using ffmpeg: {}".format( From fe3995f07de627e9866e913b1054290c10d24847 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 May 2025 13:56:43 +0200 Subject: [PATCH 4/9] Applied suggestions from @iLLicit Simplifies the ffmpeg command construction by moving the seek position argument to the beginning of the command list if a seek position is specified, leading to a clearer and more maintainable structure. This also ensures that the output path is always the last argument passed to ffmpeg. --- .../plugins/publish/extract_thumbnail.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 3626c5f381..9f58be7d94 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -503,20 +503,19 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): seek_position = duration * self.duration_split # Build command args - cmd_args = [ - "-y", + cmd_args = [] + if seek_position > 0.0: + cmd_args.extend(["--ss", str(seek_position)]) + + # Add generic ffmpeg commands + cmd_args.extend([ "-i", video_file_path, "-analyzeduration", max_int, "-probesize", max_int, - ] - - # 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"]) + "-y", + "-frames:v", "1", + output_thumb_file_path + ]) # add output file path cmd_args.append(output_thumb_file_path) From f0be8cd87704fb82ff946b50dde8418a88a95f5d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 May 2025 13:56:43 +0200 Subject: [PATCH 5/9] Applied suggestions from @iLLiCiTiT Simplifies the ffmpeg command construction by moving the seek position argument to the beginning of the command list if a seek position is specified, leading to a clearer and more maintainable structure. This also ensures that the output path is always the last argument passed to ffmpeg. --- .../plugins/publish/extract_thumbnail.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 3626c5f381..9f58be7d94 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -503,20 +503,19 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): seek_position = duration * self.duration_split # Build command args - cmd_args = [ - "-y", + cmd_args = [] + if seek_position > 0.0: + cmd_args.extend(["--ss", str(seek_position)]) + + # Add generic ffmpeg commands + cmd_args.extend([ "-i", video_file_path, "-analyzeduration", max_int, "-probesize", max_int, - ] - - # 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"]) + "-y", + "-frames:v", "1", + output_thumb_file_path + ]) # add output file path cmd_args.append(output_thumb_file_path) From 9791fda4f6a4e8b898227700f9df4a119845c97b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 May 2025 14:02:45 +0200 Subject: [PATCH 6/9] Applied suggestions from @iLLiCiTiT Ensures that any generated thumbnail files that are empty are removed to prevent issues with subsequent processing or storage. --- client/ayon_core/plugins/publish/extract_thumbnail.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 9f58be7d94..4b93b6514e 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -569,6 +569,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): error) ) return None + finally: + # Remove output file if is empty + if ( + os.path.exists(output_thumb_file_path) + and os.path.getsize(output_thumb_file_path) == 0 + ): + os.remove(output_thumb_file_path) def _get_resolution_arg( self, From d01afd073a7501152bf0a94dd81b997bde5defde Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 May 2025 16:06:53 +0200 Subject: [PATCH 7/9] Simplifies debug log message Removes unnecessary f-string formatting in a debug log message within the thumbnail extraction process. This simplifies the log output and improves readability. --- client/ayon_core/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 7f698d3b2b..2b08d663de 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -580,7 +580,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): os.path.exists(output_thumb_file_path) and os.path.getsize(output_thumb_file_path) > 0 ): - self.log.debug(f"Fallback thumbnail created") + self.log.debug("Fallback thumbnail created") return output_thumb_file_path return None except RuntimeError as error: From f5f145287222802155088755269866e6bb42ce11 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 14 May 2025 10:20:21 +0200 Subject: [PATCH 8/9] Fixes ffmpeg seek argument Corrects the ffmpeg command-line argument for specifying the seek position. It changes from '--ss' to '-ss', which is the correct flag. --- client/ayon_core/plugins/publish/extract_thumbnail.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 2b08d663de..69bb9007f9 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -525,7 +525,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # Build command args cmd_args = [] if seek_position > 0.0: - cmd_args.extend(["--ss", str(seek_position)]) + cmd_args.extend(["-ss", str(seek_position)]) # Add generic ffmpeg commands cmd_args.extend([ @@ -537,9 +537,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): output_thumb_file_path ]) - # add output file path - cmd_args.append(output_thumb_file_path) - # create ffmpeg command cmd = get_ffmpeg_tool_args( "ffmpeg", From 9b8229fa811777c1794063de63ea7be8866b57ba Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 14 May 2025 16:29:49 +0200 Subject: [PATCH 9/9] Fixes: Uses correct fallback data key The code now uses the correct key ("fallback_type") to access the fallback type from the configuration data, ensuring the correct config path is retrieved when no product is found. --- client/ayon_core/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 8c4f97ab1c..4b1d14d570 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -834,7 +834,7 @@ def _get_global_config_data( if not product_entities_by_name: # in case no product was found we need to use fallback - fallback_type = fallback_data["type"] + fallback_type = fallback_data["fallback_type"] return _get_config_path_from_profile_data( fallback_data, fallback_type, template_data )