diff --git a/src/sigal/gallery.py b/src/sigal/gallery.py index d78f3fa..ada506c 100644 --- a/src/sigal/gallery.py +++ b/src/sigal/gallery.py @@ -179,6 +179,9 @@ class Media: s['thumb_video_delay'], fit=s['thumb_fit'], converter=s['video_converter'], + black_retries=s['thumb_video_black_retries'], + black_offset=s['thumb_video_black_retry_offset'], + black_max_colors=s['thumb_video_black_max_colors'] ) except Exception as e: self.logger.error('Failed to generate thumbnail: %s', e) diff --git a/src/sigal/settings.py b/src/sigal/settings.py index 5572da8..e6d4994 100644 --- a/src/sigal/settings.py +++ b/src/sigal/settings.py @@ -75,7 +75,10 @@ _DEFAULT_CONFIG = { 'thumb_prefix': '', 'thumb_size': (200, 150), 'thumb_suffix': '', - 'thumb_video_delay': '0', + 'thumb_video_delay': 0, + 'thumb_video_black_retries': 0, + 'thumb_video_black_retry_offset': 1, + 'thumb_video_black_max_colors': 4, 'title': '', 'use_orig': False, 'user_css': None, diff --git a/src/sigal/templates/sigal.conf.py b/src/sigal/templates/sigal.conf.py index 724c0f9..b6aa5f4 100644 --- a/src/sigal/templates/sigal.conf.py +++ b/src/sigal/templates/sigal.conf.py @@ -119,7 +119,13 @@ thumb_size = (280, 210) # thumb_fit_centering = (0.5, 0.5) # Delay in seconds to avoid black thumbnails in videos with fade-in -# thumb_video_delay = '0' +# thumb_video_delay = 0 +# Max retries to generate a non-black thumbnail +# thumb_video_black_retries = 0 +# For each retry, advance another N seconds on top of original delay +# thumb_video_black_retry_offset = 1 +# A thumbnail with more than max_colors will not be considered "all black" +# thumb_video_black_max_colors = 4 # Keep original image (default: False) # keep_orig = True diff --git a/src/sigal/video.py b/src/sigal/video.py index c9dfd47..1e4f52b 100644 --- a/src/sigal/video.py +++ b/src/sigal/video.py @@ -27,6 +27,8 @@ import shutil import subprocess from os.path import splitext +from PIL import Image as PILImage + from . import image, utils from .utils import is_valid_html5_video @@ -172,17 +174,32 @@ def generate_video(source, outname, settings): def generate_thumbnail( - source, outname, box, delay, fit=True, options=None, converter="ffmpeg" + source, outname, box, delay, fit=True, options=None, converter="ffmpeg", black_retries=0, black_offset=1, black_max_colors=4 ): """Create a thumbnail image for the video source, based on ffmpeg.""" logger = logging.getLogger(__name__) tmpfile = outname + ".tmp.jpg" - # dump an image of the video - cmd = [converter, "-i", source, "-an", "-r", "1"] - cmd += ["-ss", str(delay), "-vframes", "1", "-y", tmpfile] - logger.debug("Create thumbnail for video: %s", " ".join(cmd)) - check_subprocess(cmd, source, outname) + currentTry = 0 + iDelay = int(delay) + while currentTry <= abs(black_retries): + # dump an image of the video + cmd = [converter, "-i", source, "-an", "-r", "1"] + cmd += ["-ss", str(iDelay), "-vframes", "1", "-y", tmpfile] + logger.debug("Create thumbnail for video: %s", " ".join(cmd)) + check_subprocess(cmd, source, outname) + if os.path.isfile(tmpfile) and black_retries > 0: + img = PILImage.open(tmpfile) + colors = img.getcolors(maxcolors=black_max_colors) + if colors is None: + # There were more colors than maxcolors in the image, it looks suitable for a valid thumbnail + break + else: + # Only found 'maxcolors' unique colors, looks like a solid color, try again with another seek delay + currentTry += 1 + iDelay += abs(black_offset) + else: + break # Sometimes ffmpeg fails with returncode zero but without producing an # output file Thus, we need to check if an output file was created. If @@ -228,6 +245,9 @@ def process_video(media): fit=settings["thumb_fit"], options=settings["jpg_options"], converter=settings["video_converter"], + black_retries=s['thumb_video_black_retries'], + black_offset=s['thumb_video_black_retry_offset'], + black_max_colors=s['thumb_video_black_max_colors'] ) return status.value