Improve video thumbnailer by skipping early black frames.

master
voussoir 2023-06-25 00:00:51 -07:00
parent 19a0322901
commit ec8644dded
1 changed files with 32 additions and 18 deletions

View File

@ -3,7 +3,9 @@ This file provides functions which are used in various places throughout the
codebase but don't deserve to be methods of any class. codebase but don't deserve to be methods of any class.
''' '''
import bs4 import bs4
import io
import datetime import datetime
import kkroening_ffmpeg
import hashlib import hashlib
import os import os
import PIL.Image import PIL.Image
@ -18,6 +20,9 @@ from voussoirkit import imagetools
from voussoirkit import pathclass from voussoirkit import pathclass
from voussoirkit import stringtools from voussoirkit import stringtools
from voussoirkit import timetools from voussoirkit import timetools
from voussoirkit import vlogging
log = vlogging.get_logger(__name__)
from . import constants from . import constants
from . import exceptions from . import exceptions
@ -255,13 +260,23 @@ def generate_image_thumbnail(*args, trusted_file=False, **kwargs) -> PIL.Image:
finally: finally:
PIL.Image.MAX_IMAGE_PIXELS = _max_pixels PIL.Image.MAX_IMAGE_PIXELS = _max_pixels
def image_is_mostly_black(image):
tiny = image.copy()
tiny.thumbnail((64, 64))
pixels = list(tiny.getdata())
black_count = 0
if tiny.mode == 'RGB':
black_count = sum(1 for pixel in pixels if sum(pixel) <= 24)
return (black_count / len(pixels)) > 0.5
def generate_video_thumbnail(filepath, width, height, **special) -> PIL.Image: def generate_video_thumbnail(filepath, width, height, **special) -> PIL.Image:
if not os.path.isfile(filepath): file = pathclass.Path(filepath)
raise FileNotFoundError(filepath) file.assert_is_file()
probe = constants.ffmpeg.probe(filepath) probe = constants.ffmpeg.probe(filepath)
if not probe or not probe.video: if not probe or not probe.video:
return False return None
size = imagetools.fit_into_bounds( size = imagetools.fit_into_bounds(
image_width=probe.video.video_width, image_width=probe.video.video_width,
@ -269,26 +284,25 @@ def generate_video_thumbnail(filepath, width, height, **special) -> PIL.Image:
frame_width=width, frame_width=width,
frame_height=height, frame_height=height,
) )
size = '%dx%d' % size
duration = probe.video.duration duration = probe.video.duration
if 'timestamp' in special: if 'timestamp' in special:
timestamp = special['timestamp'] timestamp_choices = [special['timestamp']]
elif duration < 3:
timestamp = 0
else: else:
timestamp = 2 timestamp_choices = list(range(0, int(duration), 3))
image = None
for this_time in timestamp_choices:
log.debug('Attempting video thumbnail at t=%d', this_time)
command = kkroening_ffmpeg.input(file.absolute_path, ss=this_time)
command = command.filter('scale', size[0], size[1])
command = command.output('pipe:', vcodec='bmp', format='image2pipe', vframes=1)
(out, trash) = command.run(capture_stdout=True, capture_stderr=True)
bio = io.BytesIO(out)
image = PIL.Image.open(bio)
if not image_is_mostly_black(image):
break
outfile = tempfile.NamedTemporaryFile(suffix='.jpg', delete=False)
constants.ffmpeg.thumbnail(
filepath,
outfile=outfile.name,
quality=2,
size=size,
time=timestamp,
)
outfile.close()
image = PIL.Image.open(outfile.name)
return image return image
def get_mimetype(extension) -> typing.Optional[str]: def get_mimetype(extension) -> typing.Optional[str]: