Skip to content

Commit a5f4e23

Browse files
GH-10: Implement the VTT generation logic and basepath option usage (GH-17)
* GH-13: Refactor the package name and move some calculations to mixin
1 parent 9582d6a commit a5f4e23

File tree

4 files changed

+85
-25
lines changed

4 files changed

+85
-25
lines changed

thumbnails.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import concurrent.futures
22

3-
from ffmpeg import FFMpeg
3+
from thumbnails import FFMpeg
44

55
# Read from the program arguments.
66
compress = 1
77
interval = 20
8+
basepath = "/stc/"
89

910
files = ["valerian-1080p.avi", "valerian-1080p.mkv", "valerian-1080p.mov", "valerian-1080p.mp4",
1011
"valerian-1080p.webm", "valerian-1080p.wmv", "valerian-1080p.mpeg", "valerian-1080p.mpg", "valerian-1080p.ogv"]
@@ -13,8 +14,10 @@
1314
def worker(video):
1415
video.set_compress(compress)
1516
video.set_interval(interval)
17+
video.set_basepath(basepath)
1618
video.extract_frames()
1719
video.join_frames()
20+
video.to_vtt()
1821

1922

2023
def main():
File renamed without changes.

ffmpeg/ffmpeg.py renamed to thumbnails/ffmpeg.py

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,24 @@
1111
from imageio.v3 import immeta
1212
from imageio_ffmpeg import get_ffmpeg_exe
1313

14-
FFMPEG_BINARY = get_ffmpeg_exe()
14+
from .thumbnails import _ThumbnailMixin
1515

16+
ffmpeg_bin = get_ffmpeg_exe()
1617

17-
class FFMpeg:
18+
19+
class FFMpeg(_ThumbnailMixin):
1820
def __init__(self, filename):
1921
self.__compress = 1
2022
self.__interval = 1
23+
self.__basepath = ""
24+
self.thumbnails = []
2125
duration, size = self._parse_metadata(filename)
26+
_ThumbnailMixin.__init__(self, size)
2227
self.tempdir = TemporaryDirectory()
28+
self.duration = int(duration + 1)
2329
self.filename = filename
24-
self.duration = duration
25-
self.size = size
30+
self._vtt_name = filename + ".vtt"
31+
self._image_name = filename + ".png"
2632

2733
def get_compress(self):
2834
return self.__compress
@@ -40,8 +46,14 @@ def set_interval(self, interval):
4046
raise TypeError("Interval must be an integer.")
4147
self.__interval = interval
4248

49+
def get_basepath(self):
50+
return self.__basepath
51+
52+
def set_basepath(self, path):
53+
self.__basepath = str(path)
54+
4355
@staticmethod
44-
def calc_columns(frames_count, width, height):
56+
def _calc_columns(frames_count, width, height):
4557
ratio = 16 / 9
4658
for col in range(1, frames_count):
4759
if (col * width) / (frames_count // col * height) > ratio:
@@ -77,7 +89,7 @@ def _parse_metadata(self, filename):
7789
duration, size = meta.get("duration"), meta.get("size")
7890

7991
if not all((duration, size)):
80-
cmd = (FFMPEG_BINARY, "-hide_banner", "-i", filename)
92+
cmd = (ffmpeg_bin, "-hide_banner", "-i", filename)
8193

8294
popen_params = self._cross_platform_popen_params()
8395
process = subprocess.Popen(cmd, **popen_params)
@@ -95,7 +107,7 @@ def _extract_frame(self, start_time):
95107
_timestamp = str(timedelta(seconds=start_time))
96108

97109
cmd = (
98-
FFMPEG_BINARY,
110+
ffmpeg_bin,
99111
"-ss", _timestamp,
100112
"-i", _input_file,
101113
"-loglevel", "error",
@@ -107,30 +119,27 @@ def _extract_frame(self, start_time):
107119
subprocess.Popen(cmd).wait()
108120

109121
def extract_frames(self):
110-
_intervals = range(0, int(self.duration), self.get_interval())
122+
_intervals = range(0, self.duration, self.get_interval())
111123
with concurrent.futures.ThreadPoolExecutor() as executor:
112124
executor.map(self._extract_frame, _intervals)
113125

114126
def join_frames(self):
115-
width, height = self.size
116-
117-
_min_width = 300
118-
_min_height = math.ceil(_min_width * height / width)
119-
120-
width = max(_min_width, width * self.__compress / 10)
121-
height = max(_min_height, height * self.__compress / 10)
122-
123127
line, column = 0, 0
124128
frames = sorted(glob.glob(self.tempdir.name + os.sep + "*.png"))
125-
frames_count = len(range(0, int(self.duration), self.get_interval()))
126-
columns = self.calc_columns(frames_count, width, height)
127-
master_height = height * int(math.ceil(float(frames_count) / columns))
128-
master = Image.new(mode="RGBA", size=(width * columns, master_height))
129+
frames_count = len(range(0, self.duration, self.get_interval()))
130+
columns = self._calc_columns(frames_count, self.width, self.height)
131+
master_height = self.height * int(math.ceil(float(frames_count) / columns))
132+
master = Image.new(mode="RGBA", size=(self.width * columns, master_height))
129133

130-
for frame in frames:
134+
for n, frame in enumerate(frames):
131135
with Image.open(frame) as image:
132-
x, y = width * column, height * line
133-
image = image.resize((width, height), Image.ANTIALIAS)
136+
x, y = self.width * column, self.height * line
137+
138+
start = n * self.get_interval()
139+
end = (n + 1) * self.get_interval()
140+
self.thumbnails.append((start, end, x, y))
141+
142+
image = image.resize((self.width, self.height), Image.ANTIALIAS)
134143
master.paste(image, (x, y))
135144

136145
column += 1
@@ -139,5 +148,22 @@ def join_frames(self):
139148
line += 1
140149
column = 0
141150

142-
master.save(self.filename + ".png")
151+
master.save(self._image_name)
143152
self.tempdir.cleanup()
153+
154+
def to_vtt(self):
155+
def _format_time(seconds):
156+
return "0%s.000" % str(timedelta(seconds=seconds))
157+
158+
_lines = ["WEBVTT\n\n"]
159+
_img_src = self.get_basepath() + self._image_name
160+
161+
for start, end, x, y in self.thumbnails:
162+
_thumbnail = "%s --> %s\n%s#xywh=%d,%d,%d,%d\n\n" % (
163+
_format_time(start), _format_time(end),
164+
_img_src, x, y, self.width, self.height
165+
)
166+
_lines.append(_thumbnail)
167+
168+
with open(self._vtt_name, "w") as vtt:
169+
vtt.writelines(_lines)

thumbnails/thumbnails.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import math
2+
3+
4+
class _ThumbnailMixin:
5+
def __init__(self, size):
6+
self._w = None
7+
self._h = None
8+
9+
width, height = size
10+
_min_width = 300
11+
_min_height = math.ceil(_min_width * height / width)
12+
13+
self._width = width / 10
14+
self._height = height / 10
15+
self._min_width = _min_width
16+
self._min_height = _min_height
17+
18+
def get_compress(self):
19+
raise NotImplementedError
20+
21+
@property
22+
def width(self):
23+
if not self._w:
24+
self._w = max(self._min_width, self._width * self.get_compress())
25+
return self._w
26+
27+
@property
28+
def height(self):
29+
if not self._h:
30+
self._h = max(self._min_height, self._height * self.get_compress())
31+
return self._h

0 commit comments

Comments
 (0)