Skip to content

Commit

Permalink
fix(ffmpeg): raise if time for thumbnail generation is invalid
Browse files Browse the repository at this point in the history
  • Loading branch information
escaped committed Dec 4, 2018
1 parent 37c7abf commit 50d228d
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 11 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
CHANGELOG
=======

0.4.0 (04.12.2018)
----------

* Change: An `InvalidTimeError` is raise, when a thumbnail could not be generated
* This can happens if the chosen time is too close to the end of the video
or if the video is shorter.


0.3.1 (16.11.2018)
----------

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-video-encoding"
version = "0.3.1"
version = "0.4.0"
description = "django-video-encoding helps to convert your videos into different formats and resolutions."
authors = ["Alexander Frenzel <alex@relatedworks.com>"]

Expand Down
23 changes: 23 additions & 0 deletions test_proj/media_library/tests/test_ffmpeg_backend.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import os
import tempfile

import pytest
from PIL import Image

from video_encoding import exceptions
from video_encoding.backends.ffmpeg import FFmpegBackend


Expand Down Expand Up @@ -46,6 +48,27 @@ def test_get_thumbnail(ffmpeg, video_path):
assert height == 720


def test_get_thumbnail__invalid_time(ffmpeg, video_path):
with pytest.raises(exceptions.InvalidTimeError):
ffmpeg.get_thumbnail(video_path, at_time=1000000)


@pytest.mark.parametrize(
'offset', (0, 0.02),
)
def test_get_thumbnail__too_close_to_the_end(ffmpeg, video_path, offset):
"""
If the selected time point is close to the end of the video,
a video frame cannot be extracted.
"""
duration = ffmpeg.get_media_info(video_path)['duration']

with pytest.raises(exceptions.InvalidTimeError):
ffmpeg.get_thumbnail(
video_path, at_time=duration - offset,
)


def test_check():
assert FFmpegBackend.check() == []

Expand Down
3 changes: 3 additions & 0 deletions video_encoding/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@ def get_media_info(self, video_path): # pragma: no cover
def get_thumbnail(self, video_path): # pragma: no cover
"""
Extracts an image of a video and returns its path.
If the requested thumbnail is not within the duration of the video
an `InvalidTimeError` is thrown.
"""
pass
28 changes: 19 additions & 9 deletions video_encoding/backends/ffmpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import six
from django.core import checks

from .. import exceptions
from ..compat import which
from ..config import settings
from ..exceptions import FFmpegError
from .base import BaseEncodingBackend

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -39,19 +39,19 @@ def __init__(self):
settings, 'VIDEO_ENCODING_FFPROBE_PATH', which('ffprobe'))

if not self.ffmpeg_path:
raise FFmpegError("ffmpeg binary not found: {}".format(
raise exceptions.FFmpegError("ffmpeg binary not found: {}".format(
self.ffmpeg_path or ''))

if not self.ffprobe_path:
raise FFmpegError("ffprobe binary not found: {}".format(
raise exceptions.FFmpegError("ffprobe binary not found: {}".format(
self.ffmpeg_path or ''))

@classmethod
def check(cls):
errors = super(FFmpegBackend, cls).check()
try:
FFmpegBackend()
except FFmpegError as e:
except exceptions.FFmpegError as e:
errors.append(checks.Error(
e.msg,
hint="Please install ffmpeg.",
Expand All @@ -69,12 +69,12 @@ def _spawn(self, cmds):
)
except OSError as e:
raise six.raise_from(
FFmpegError('Error while running ffmpeg binary'), e)
exceptions.FFmpegError('Error while running ffmpeg binary'), e)

def _check_returncode(self, process):
stdout, stderr = process.communicate()
if process.returncode != 0:
raise FFmpegError("`{}` exited with code {:d}".format(
raise exceptions.FFmpegError("`{}` exited with code {:d}".format(
' '.join(process.args), process.returncode))
self.stdout = stdout.decode(console_encoding)
self.stderr = stderr.decode(console_encoding)
Expand Down Expand Up @@ -127,14 +127,14 @@ def encode(self, source_path, target_path, params): # NOQA: C901
yield percent

if os.path.getsize(target_path) == 0:
raise FFmpegError("File size of generated file is 0")
raise exceptions.FFmpegError("File size of generated file is 0")

# wait for process to exit
self._check_returncode(process)

logger.debug(output)
if not output:
raise FFmpegError("No output from FFmpeg.")
raise exceptions.FFmpegError("No output from FFmpeg.")

yield 100

Expand Down Expand Up @@ -171,18 +171,28 @@ def get_media_info(self, video_path):
def get_thumbnail(self, video_path, at_time=0.5):
"""
Extracts an image of a video and returns its path.
If the requested thumbnail is not within the duration of the video
an `InvalidTimeError` is thrown.
"""
filename = os.path.basename(video_path)
filename, __ = os.path.splitext(filename)
_, image_path = tempfile.mkstemp(suffix='_{}.jpg'.format(filename))

video_duration = self.get_media_info(video_path)['duration']
thumbnail_time = min(at_time, video_duration - 0.02)
if at_time > video_duration:
raise exceptions.InvalidTimeError()
thumbnail_time = at_time

cmds = [self.ffmpeg_path, '-i', video_path, '-vframes', '1']
cmds.extend(['-ss', str(thumbnail_time), '-y', image_path])

process = self._spawn(cmds)
self._check_returncode(process)

if not os.path.getsize(image_path):
# we somehow failed to generate thumbnail
os.unlink(image_path)
raise exceptions.InvalidTimeError()

return image_path
6 changes: 5 additions & 1 deletion video_encoding/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
class VideoEncodingError(Exception):
pass


class FFmpegError(VideoEncodingError):
def __init__(self, *args, **kwargs):
self.msg = args[0]
super(VideoEncodingError, self).__init__(*args, **kwargs)


class FFmpegError(VideoEncodingError):
class InvalidTimeError(VideoEncodingError):
pass

0 comments on commit 50d228d

Please sign in to comment.