1
'''Generates thumbnails from video files'''
2
# pylint: disable-msg=C0301
5
__copyright__ = "2007, Paul Hummer"
6
__author__ = "Paul Hummer <paul@ironlionsoftware.no.spam.com>"
8
# I think it's important to note that the methodology of this code (and
9
# some actual EXACT snippets of code) were based on Elisa
10
# (http://elisa.fluendo.com/)
13
from threading import Event
20
from entertainerlib.exceptions import VideoThumbnailerException
21
from entertainerlib.utils import thumbnailer
24
class VideoThumbnailer(thumbnailer.Thumbnailer):
25
'''Create thumbnails from videos.'''
27
class VideoSinkBin(gst.Bin):
28
'''A gstreamer sink bin'''
30
def __init__(self, needed_caps):
32
gst.Bin.__init__(self)
33
self._capsfilter = gst.element_factory_make(
34
'capsfilter', 'capsfilter')
36
self.set_caps(needed_caps)
37
self.add(self._capsfilter)
39
fakesink = gst.element_factory_make('fakesink', 'fakesink')
40
fakesink.set_property("sync", False)
42
self._capsfilter.link(fakesink)
44
pad = self._capsfilter.get_pad("sink")
45
ghostpad = gst.GhostPad("sink", pad)
47
pad2probe = fakesink.get_pad("sink")
48
pad2probe.add_buffer_probe(self.buffer_probe)
50
self.add_pad(ghostpad)
51
self.sink = self._capsfilter
53
def set_current_frame(self, value):
54
'''Set the current frame'''
55
self._current_frame = value
57
def set_caps(self, caps):
58
'''Set the bin caps'''
59
gst_caps = gst.caps_from_string(caps)
60
self._capsfilter.set_property("caps", gst_caps)
62
def get_current_frame(self):
63
'''Gets the current frame'''
64
frame = self._current_frame
65
self._current_frame = None
68
def buffer_probe(self, pad, buff):
69
'''Buffer the probe'''
73
self.width = s['width']
74
self.height = s['height']
75
if self.width != None and self.height != None and buff != None:
76
self.set_current_frame(buff.data)
83
self.set_current_frame(None)
86
def __init__(self, filename, src="video"):
88
thumbnailer.Thumbnailer.__init__(self, filename, src)
89
self._fileuri = 'file://%s' % (self.filename)
91
#Initialize and use the gstreamer pipeline
92
self._pipeline = gst.element_factory_make('playbin', 'playbin')
93
self._sink = self.VideoSinkBin('video/x-raw-rgb,bpp=24,depth=24')
94
self._blocker = Event()
95
self._pipeline.set_property('video-sink', self._sink)
96
self._pipeline.set_property('volume', 0)
99
self.BORING_THRESHOLD = 2000
100
self.HOLES_SIZE = (9, 35)
101
self.HOLES_DATA = '\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x9a\x9a\x9a\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x91\x91\x91\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x9a\x9a\x9a\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x91\x91\x91\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x9a\x9a\x9a\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x91\x91\x91\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x9a\x9a\x9a\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x91\x91\x91\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x9a\x9a\x9a\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x91\x91\x91\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6'
103
def add_holes(self, img):
104
'''Add the holes image'''
105
holes = Image.fromstring('RGBA', self.HOLES_SIZE, self.HOLES_DATA)
106
holes_h = holes.size[1]
107
remain = img.size[1] % holes_h
111
while i < (img.size[1] - remain):
112
left_box = (0, i, holes.size[0], (nbands+1) * holes.size[1])
113
img.paste(holes, left_box)
115
right_box = (img.size[0] - holes.size[0], i,
116
img.size[0], (nbands+1) * holes.size[1])
117
img.paste(holes, right_box)
122
remain_holes = holes.crop((0, 0, holes.size[0], remain))
124
img.paste(remain_holes, (0, i, holes.size[0], img.size[1]))
125
img.paste(remain_holes, (img.size[0] - holes.size[0], i,
126
img.size[0], img.size[1]))
129
def interesting_image(self, img):
131
Checks an image to see if it has the characteristics of an
132
'interesting' image, i.e. whether or not the image is worth
133
examining for thumbnailing
135
stat = ImageStat.Stat(img)
136
return True in [ i > self.BORING_THRESHOLD for i in stat.var ]
138
def set_pipeline_state(self, pipeline, state):
140
Does exactly what it claims : sets the state of the pipeline, and
141
returns the boolean value of whether or not is successfully changed
144
status = pipeline.set_state(state)
145
if status == gst.STATE_CHANGE_ASYNC:
150
while not result[0] == gst.STATE_CHANGE_SUCCESS:
155
result = pipeline.get_state(50*gst.MSECOND)
158
elif status == gst.STATE_CHANGE_SUCCESS:
164
def create_thumbnail(self):
166
Creates the new thumbnail based on the information provided through the
167
gstreamer API. The file is an md5 of the video's file name, followed
168
by the .jpg extension
171
if os.path.exists(self._thumb_file):
174
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
175
self._pipeline.set_property('uri', self._fileuri)
177
if not self.set_pipeline_state(self._pipeline, gst.STATE_PAUSED):
178
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
179
raise VideoThumbnailerException('Cannot start the pipeline')
181
if self._sink.width == None or self._sink.height == None:
182
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
183
raise VideoThumbnailerException('Unable to determine media size')
184
sink_size = (self._sink.width, self._sink.height)
187
duration = self._pipeline.query_duration(gst.FORMAT_TIME)[0]
188
except AssertionError:
189
#Gstreamer cannot determine the media duration using
190
#playing-thumbnailing for file
191
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
193
img = self._play_for_thumb(sink_size, 0)
195
img.save(self._thumb_file)
198
duration /= gst.NSECOND
200
img = self._seek_for_thumb(duration, sink_size)
203
img.save(self._thumb_file)
205
except VideoThumbnailerException:
206
#Fallback: No Image found in seek_for, falling back to
208
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
209
img = self._play_for_thumb(sink_size, duration)
210
#Fallback-Play found img
212
img.save(self._thumb_file)
215
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
216
raise VideoThumbnailerException(
217
'Unable to create thumbnail. Please file a bug')
220
def _play_for_thumb(self, sink_size, duration=0):
222
Plays the video file to gather information for generating a thumbnail
226
if duration >= 250000:
228
elif duration >= 200000:
230
elif duration >= 10000:
232
elif duration >= 5000:
237
#Setting every-frame to self._every
239
self._every_co = self._every
241
## How often Proceed?
244
self.set_state_blocking(self._pipeline, gst.STATE_PLAYING)
247
self._pipeline.set_state(gst.STATE_NULL)
250
def _seek_for_thumb(self, duration, sink_size):
252
Seeks through the video file to gather information for generating a
255
frame_locations = [ 1.0 / 3.0, 2.0 / 3.0, 0.1, 0.9, 0.5 ]
257
for location in frame_locations:
258
abs_location = int(location * duration)
260
if abs_location == 0:
261
raise VideoThumbnailerException(
262
self.filename, 'Video has a size of zero')
264
event = self._pipeline.seek(1.0, gst.FORMAT_TIME,
265
gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT,
266
gst.SEEK_TYPE_SET, abs_location,
267
gst.SEEK_TYPE_NONE, 0)
269
raise VideoThumbnailerException(self.filename,
270
'Unable to seek through video')
272
if not self.set_pipeline_state(self._pipeline, gst.STATE_PAUSED):
273
raise VideoThumbnailerException(self.filename,
274
'Unable to pause video')
276
frame = self._sink.get_current_frame()
278
img = Image.frombuffer(
279
"RGB", sink_size, frame, "raw", "RGB", 0, 1)
281
if self.interesting_image(img):
289
img.thumbnail((self.MAX_SIZE, self.MAX_SIZE), Image.BILINEAR)
290
if img.mode != 'RGBA':
291
img = img.convert(mode='RGBA')
292
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
295
gobject.type_register(VideoThumbnailer.VideoSinkBin)