~khwalker/entertainer/config-keybindings

« back to all changes in this revision

Viewing changes to entertainerlib/utils/video_thumbnailer.py

The thumbnailers have all been consolidated into a single module.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
'''Generates thumbnails from video files'''
2
 
# pylint: disable-msg=C0301
3
 
 
4
 
__license__ = "GPLv2"
5
 
__copyright__ = "2007, Paul Hummer"
6
 
__author__ = "Paul Hummer <paul@ironlionsoftware.no.spam.com>"
7
 
 
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/)
11
 
 
12
 
import os
13
 
from threading import Event
14
 
 
15
 
import gobject
16
 
import gst
17
 
import Image
18
 
import ImageStat
19
 
 
20
 
from entertainerlib.exceptions import VideoThumbnailerException
21
 
from entertainerlib.utils import thumbnailer
22
 
 
23
 
 
24
 
class VideoThumbnailer(thumbnailer.Thumbnailer):
25
 
    '''Create thumbnails from videos.'''
26
 
 
27
 
    class VideoSinkBin(gst.Bin):
28
 
        '''A gstreamer sink bin'''
29
 
 
30
 
        def __init__(self, needed_caps):
31
 
            self.reset()
32
 
            gst.Bin.__init__(self)
33
 
            self._capsfilter = gst.element_factory_make(
34
 
                'capsfilter', 'capsfilter')
35
 
 
36
 
            self.set_caps(needed_caps)
37
 
            self.add(self._capsfilter)
38
 
 
39
 
            fakesink = gst.element_factory_make('fakesink', 'fakesink')
40
 
            fakesink.set_property("sync", False)
41
 
            self.add(fakesink)
42
 
            self._capsfilter.link(fakesink)
43
 
 
44
 
            pad = self._capsfilter.get_pad("sink")
45
 
            ghostpad = gst.GhostPad("sink", pad)
46
 
 
47
 
            pad2probe = fakesink.get_pad("sink")
48
 
            pad2probe.add_buffer_probe(self.buffer_probe)
49
 
 
50
 
            self.add_pad(ghostpad)
51
 
            self.sink = self._capsfilter
52
 
 
53
 
        def set_current_frame(self, value):
54
 
            '''Set the current frame'''
55
 
            self._current_frame = value
56
 
 
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)
61
 
 
62
 
        def get_current_frame(self):
63
 
            '''Gets the current frame'''
64
 
            frame = self._current_frame
65
 
            self._current_frame = None
66
 
            return frame
67
 
 
68
 
        def buffer_probe(self, pad, buff):
69
 
            '''Buffer the probe'''
70
 
            caps = buff.caps
71
 
            if caps != None:
72
 
                s = caps[0]
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)
77
 
            return True
78
 
 
79
 
        def reset(self):
80
 
            '''Reset the bin'''
81
 
            self.width = None
82
 
            self.height = None
83
 
            self.set_current_frame(None)
84
 
 
85
 
 
86
 
    def __init__(self, filename, src="video"):
87
 
 
88
 
        thumbnailer.Thumbnailer.__init__(self, filename, src)
89
 
        self._fileuri = 'file://%s' % (self.filename)
90
 
 
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)
97
 
 
98
 
 
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'
102
 
 
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
108
 
 
109
 
        i = 0
110
 
        nbands = 0
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)
114
 
 
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)
118
 
 
119
 
            i += holes_h
120
 
            nbands += 1
121
 
 
122
 
        remain_holes = holes.crop((0, 0, holes.size[0], remain))
123
 
        remain_holes.load()
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]))
127
 
        return img
128
 
 
129
 
    def interesting_image(self, img):
130
 
        '''
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
134
 
        '''
135
 
        stat = ImageStat.Stat(img)
136
 
        return True in [ i > self.BORING_THRESHOLD for i in stat.var ]
137
 
 
138
 
    def set_pipeline_state(self, pipeline, state):
139
 
        '''
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
142
 
        state
143
 
        '''
144
 
        status = pipeline.set_state(state)
145
 
        if status == gst.STATE_CHANGE_ASYNC:
146
 
 
147
 
            result = [False]
148
 
            max_try = 100
149
 
            nb_try = 0
150
 
            while not result[0] == gst.STATE_CHANGE_SUCCESS:
151
 
                if nb_try > max_try:
152
 
                    #State change failed
153
 
                    return False
154
 
                nb_try += 1
155
 
                result = pipeline.get_state(50*gst.MSECOND)
156
 
 
157
 
            return True
158
 
        elif status == gst.STATE_CHANGE_SUCCESS:
159
 
            return True
160
 
        else:
161
 
            return False
162
 
 
163
 
 
164
 
    def create_thumbnail(self):
165
 
        '''
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
169
 
        '''
170
 
 
171
 
        if os.path.exists(self._thumb_file):
172
 
            return
173
 
 
174
 
        self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
175
 
        self._pipeline.set_property('uri', self._fileuri)
176
 
 
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')
180
 
 
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)
185
 
 
186
 
        try:
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)
192
 
 
193
 
            img = self._play_for_thumb(sink_size, 0)
194
 
            if img:
195
 
                img.save(self._thumb_file)
196
 
                return
197
 
        else:
198
 
            duration /= gst.NSECOND
199
 
            try:
200
 
                img = self._seek_for_thumb(duration, sink_size)
201
 
                #Seek found image
202
 
                if img:
203
 
                    img.save(self._thumb_file)
204
 
                    return
205
 
            except VideoThumbnailerException:
206
 
                #Fallback: No Image found in seek_for, falling back to
207
 
                #play_for_thumb
208
 
                self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
209
 
                img = self._play_for_thumb(sink_size, duration)
210
 
                #Fallback-Play found img
211
 
                if img:
212
 
                    img.save(self._thumb_file)
213
 
                    return
214
 
 
215
 
        self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
216
 
        raise VideoThumbnailerException(
217
 
            'Unable to create thumbnail.  Please file a bug')
218
 
 
219
 
 
220
 
    def _play_for_thumb(self, sink_size, duration=0):
221
 
        '''
222
 
        Plays the video file to gather information for generating a thumbnail
223
 
        '''
224
 
        self._img = None
225
 
 
226
 
        if duration >= 250000:
227
 
            self._every = 25
228
 
        elif duration >= 200000:
229
 
            self._every = 15
230
 
        elif duration >= 10000:
231
 
            self._every = 10
232
 
        elif duration >= 5000:
233
 
            self._every = 5
234
 
        else:
235
 
            self._every = 1
236
 
 
237
 
        #Setting every-frame to self._every
238
 
 
239
 
        self._every_co = self._every
240
 
 
241
 
        ## How often Proceed?
242
 
        self._counter = 5
243
 
 
244
 
        self.set_state_blocking(self._pipeline, gst.STATE_PLAYING)
245
 
 
246
 
        self._blocker.wait()
247
 
        self._pipeline.set_state(gst.STATE_NULL)
248
 
        return self._img
249
 
 
250
 
    def _seek_for_thumb(self, duration, sink_size):
251
 
        '''
252
 
        Seeks through the video file to gather information for generating a
253
 
        thumbnail
254
 
        '''
255
 
        frame_locations = [ 1.0 / 3.0, 2.0 / 3.0, 0.1, 0.9, 0.5 ]
256
 
 
257
 
        for location in frame_locations:
258
 
            abs_location = int(location * duration)
259
 
 
260
 
            if abs_location == 0:
261
 
                raise VideoThumbnailerException(
262
 
                    self.filename, 'Video has a size of zero')
263
 
 
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)
268
 
            if not event:
269
 
                raise VideoThumbnailerException(self.filename,
270
 
                    'Unable to seek through video')
271
 
 
272
 
            if not self.set_pipeline_state(self._pipeline, gst.STATE_PAUSED):
273
 
                raise VideoThumbnailerException(self.filename,
274
 
                    'Unable to pause video')
275
 
 
276
 
            frame = self._sink.get_current_frame()
277
 
 
278
 
            img = Image.frombuffer(
279
 
                "RGB", sink_size, frame, "raw", "RGB", 0, 1)
280
 
 
281
 
            if self.interesting_image(img):
282
 
                break
283
 
            else:
284
 
                pass
285
 
 
286
 
        self._sink.reset()
287
 
 
288
 
        if 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)
293
 
            return img
294
 
 
295
 
gobject.type_register(VideoThumbnailer.VideoSinkBin)