- Refractored the video thumbnailer
authorJoar Wandborg <git@wandborg.com>
Tue, 11 Oct 2011 02:57:17 +0000 (04:57 +0200)
committerJoar Wandborg <git@wandborg.com>
Tue, 11 Oct 2011 02:57:17 +0000 (04:57 +0200)
 - Started work on video transcoder
   Not done, by far!
 - Bug fix in video.processing error handling

mediagoblin/media_types/video/processing.py
mediagoblin/media_types/video/transcoders.py

index d7a48caa6349300da82d0c5b008c3afb93374b51..52047ae442e9359b4fd861af4a05b4b45bd5c2ed 100644 (file)
@@ -213,7 +213,6 @@ def _transcoding_complete(*args):
 def _transcoding_error(queue, qentry, arg, info):
     logger.info('Error')
     __close_processing(queue, qentry, info, error=True)
-    logger.debug((queue, quentry, info, arg))
 
 
 def _transcoding_pass_setup(queue, qentry, options):
index 1134bc668ce184b1264db6d36eee4988d11ac58f..d305d5fce84463608c93addeaffa8adbccb8ac47 100644 (file)
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+from __future__ import division
 import sys
 import logging
 
+
 _log = logging.getLogger(__name__)
 logging.basicConfig()
-_log.setLevel(logging.INFO)
+_log.setLevel(logging.DEBUG)
 
 try:
     import gobject
+    gobject.threads_init()
 except:
     _log.error('Could not import gobject')
 
@@ -30,24 +33,83 @@ try:
     import pygst
     pygst.require('0.10')
     import gst
+    from gst.extend import discoverer
 except:
     _log.error('pygst could not be imported')
 
 
 class VideoThumbnailer:
-    def __init__(self, src, dst):
-        self._set_up_pass(src, dst)
+    '''
+    Creates a video thumbnail
+
+     - Sets up discoverer & transcoding pipeline.
+       Discoverer finds out information about the media file
+     - Launches gobject.MainLoop, this triggers the discoverer to start running
+     - Once the discoverer is done, it calls the __discovered callback function
+     - The __discovered callback function launches the transcoding process
+     - The _on_message callback is called from the transcoding process until it gets a 
+       message of type gst.MESSAGE_EOS, then it calls __stop which shuts down the
+       gobject.MainLoop
+    '''
+    def __init__(self, src, dst, **kwargs):
+        _log.info('Initializing VideoThumbnailer...')
 
         self.loop = gobject.MainLoop()
+        self.source_path = src
+        self.destination_path = dst
+
+        self.destination_dimensions = kwargs.get('dimensions') or (180, 180)
+
+        if not type(self.destination_dimensions) == tuple:
+            raise Exception('dimensions must be tuple: (width, height)')
+
+        self._setup()
+        self._run()
+
+    def _setup(self):
+        self._setup_pass()
+        self._setup_discover()
+
+    def _run(self):
+        _log.info('Discovering...')
+        self.discoverer.discover()
+        _log.info('Done')
+
+        _log.debug('Initializing MainLoop()')
         self.loop.run()
 
-    def _set_up_pass(self, src, dst):
-        self.pipeline = gst.Pipeline('TranscodingPipeline')
+    def _setup_discover(self):
+        self.discoverer = discoverer.Discoverer(self.source_path)
+
+        # Connect self.__discovered to the 'discovered' event
+        self.discoverer.connect('discovered', self.__discovered)
+
+    def __discovered(self, data, is_media):
+        '''
+        Callback for media discoverer.
+        '''
+        if not is_media:
+            self.__stop()
+            raise Exception('Could not discover {0}'.format(self.source_path))
+
+        _log.debug('__discovered, data: {0}'.format(data))
+
+        self.data = data
 
-        _log.debug('Pipeline: {0}'.format(self.pipeline))
+        self._on_discovered()
+
+        # Tell the transcoding pipeline to start running
+        self.pipeline.set_state(gst.STATE_PLAYING)
+        _log.info('Transcoding...')
+
+    def _on_discovered(self):
+        self.__setup_capsfilter()
+
+    def _setup_pass(self):
+        self.pipeline = gst.Pipeline('VideoThumbnailerPipeline')
 
         self.filesrc = gst.element_factory_make('filesrc', 'filesrc')
-        self.filesrc.set_property('location', src)
+        self.filesrc.set_property('location', self.source_path)
         self.pipeline.add(self.filesrc)
 
         self.decoder = gst.element_factory_make('decodebin2', 'decoder')
@@ -59,18 +121,17 @@ class VideoThumbnailer:
         self.pipeline.add(self.ffmpegcolorspace)
 
         self.videoscale = gst.element_factory_make('videoscale', 'videoscale')
+        self.videoscale.set_property('method', 'bilinear')
         self.pipeline.add(self.videoscale)
 
         self.capsfilter = gst.element_factory_make('capsfilter', 'capsfilter')
-        # FIXME: videoscale doesn't care about original ratios
-        self.capsfilter.set_property('caps', gst.caps_from_string('video/x-raw-rgb, width=180, height=100'))
         self.pipeline.add(self.capsfilter)
 
         self.jpegenc = gst.element_factory_make('jpegenc', 'jpegenc')
         self.pipeline.add(self.jpegenc)
 
         self.filesink = gst.element_factory_make('filesink', 'filesink')
-        self.filesink.set_property('location', dst)
+        self.filesink.set_property('location', self.destination_path)
         self.pipeline.add(self.filesink)
 
         # Link all the elements together
@@ -80,20 +141,50 @@ class VideoThumbnailer:
         self.capsfilter.link(self.jpegenc)
         self.jpegenc.link(self.filesink)
 
-        bus = self.pipeline.get_bus()
-        bus.add_signal_watch()
-        bus.connect('message', self._on_message)
+        self._setup_bus()
 
-        self.pipeline.set_state(gst.STATE_PLAYING)
+    def _setup_bus(self):
+        self.bus = self.pipeline.get_bus()
+        self.bus.add_signal_watch()
+        self.bus.connect('message', self._on_message)
+
+    def __setup_capsfilter(self):
+        thumbsizes = self.calculate_resize()  # Returns tuple with (width, height)
+
+        self.capsfilter.set_property(
+            'caps',
+            gst.caps_from_string('video/x-raw-rgb, width={width}, height={height}'.format(
+                    width=thumbsizes[0],
+                    height=thumbsizes[1]
+                    )))
 
+    def calculate_resize(self):
+        x_ratio = self.destination_dimensions[0] / self.data.videowidth
+        y_ratio = self.destination_dimensions[1] / self.data.videoheight
+
+        if self.data.videoheight > self.data.videowidth:
+            # We're dealing with a portrait!
+            dimensions = (
+                int(self.data.videowidth * y_ratio),
+                180)
+        else:
+            dimensions = (
+                180,
+                int(self.data.videoheight * x_ratio))
+
+        return dimensions
 
     def _on_message(self, bus, message):
-        _log.info((bus, message))
+        _log.debug((bus, message))
 
         t = message.type
 
         if t == gst.MESSAGE_EOS:
-            self.__shutdown()
+            self.__stop()
+            _log.info('Done')
+        elif t == gst.MESSAGE_ERROR:
+            _log.error((bus, message))
+            self.__stop()
 
     def _on_dynamic_pad(self, dbin, pad, islast):
         '''
@@ -102,7 +193,163 @@ class VideoThumbnailer:
         pad.link(
             self.ffmpegcolorspace.get_pad('sink'))
 
-    def __shutdown(self):
+    def __stop(self):
+        _log.debug(self.loop)
+
+        self.pipeline.set_state(gst.STATE_NULL)
+
+        gobject.idle_add(self.loop.quit)
+
+
+class VideoTranscoder():
+    '''
+    Video transcoder
+
+    TODO:
+     - Currently not working
+    '''
+    def __init__(self, src, dst, **kwargs):
+        _log.info('Initializing VideoTranscoder...')
+
+        self.loop = gobject.MainLoop()
+        self.source_path = src
+        self.destination_path = dst
+
+        self.destination_dimensions = kwargs.get('dimensions') or (180, 180)
+
+        if not type(self.destination_dimensions) == tuple:
+            raise Exception('dimensions must be tuple: (width, height)')
+
+        self._setup()
+        self._run()
+
+    def _setup(self):
+        self._setup_pass()
+        self._setup_discover()
+
+    def _run(self):
+        _log.info('Discovering...')
+        self.discoverer.discover()
+        _log.info('Done')
+
+        _log.debug('Initializing MainLoop()')
+        self.loop.run()
+
+    def _setup_discover(self):
+        self.discoverer = discoverer.Discoverer(self.source_path)
+
+        # Connect self.__discovered to the 'discovered' event
+        self.discoverer.connect('discovered', self.__discovered)
+
+    def __discovered(self, data, is_media):
+        '''
+        Callback for media discoverer.
+        '''
+        if not is_media:
+            self.__stop()
+            raise Exception('Could not discover {0}'.format(self.source_path))
+
+        _log.debug('__discovered, data: {0}'.format(data))
+
+        self.data = data
+
+        # Tell the transcoding pipeline to start running
+        self.pipeline.set_state(gst.STATE_PLAYING)
+        _log.info('Transcoding...')
+
+    def _on_discovered(self):
+        self.__setup_capsfilter()
+
+    def _setup_pass(self):
+        self.pipeline = gst.Pipeline('VideoTranscoderPipeline')
+
+        self.filesrc = gst.element_factory_make('filesrc', 'filesrc')
+        self.filesrc.set_property('location', self.source_path)
+        self.pipeline.add(self.filesrc)
+
+        self.decoder = gst.element_factory_make('decodebin2', 'decoder')
+
+        self.decoder.connect('new-decoded-pad', self._on_dynamic_pad)
+        self.pipeline.add(self.decoder)
+
+        self.ffmpegcolorspace = gst.element_factory_make('ffmpegcolorspace', 'ffmpegcolorspace')
+        self.pipeline.add(self.ffmpegcolorspace)
+
+        self.videoscale = gst.element_factory_make('videoscale', 'videoscale')
+        self.videoscale.set_property('method', 'bilinear')
+        self.pipeline.add(self.videoscale)
+
+        self.capsfilter = gst.element_factory_make('capsfilter', 'capsfilter')
+        self.pipeline.add(self.capsfilter)
+
+        self.vp8enc = gst.element_factory_make('vp8enc', 'vp8enc')
+        self.vp8enc.set_property('quality', 6)
+        self.vp8enc.set_property('threads', 2)
+        self.vp8enc.set_property('speed', 2)
+
+        self.webmmux = gst.element_factory_make('webmmux', 'webmmux')
+        self.pipeline.add(self.webmmux)
+
+        self.filesink = gst.element_factory_make('filesink', 'filesink')
+
+        self.filesrc.link(self.decoder)
+        self.ffmpegcolorspace.link(self.videoscale)
+        self.videoscale.link(self.capsfilter)
+        self.vp8enc.link(self.filesink)
+
+        self._setup_bus()
+
+    def _on_dynamic_pad(self, dbin, pad, islast):
+        '''
+        Callback called when ``decodebin2`` has a pad that we can connect to
+        '''
+        pad.link(
+            self.ffmpegcolorspace.get_pad('sink'))
+
+    def _setup_bus(self):
+        self.bus = self.pipeline.get_bus()
+        self.bus.add_signal_watch()
+        self.bus.connect('message', self._on_message)
+
+    def __setup_capsfilter(self):
+        thumbsizes = self.calculate_resize()  # Returns tuple with (width, height)
+
+        self.capsfilter.set_property(
+            'caps',
+            gst.caps_from_string('video/x-raw-rgb, width={width}, height={height}'.format(
+                    width=thumbsizes[0],
+                    height=thumbsizes[1]
+                    )))
+
+    def calculate_resize(self):
+        x_ratio = self.destination_dimensions[0] / self.data.videowidth
+        y_ratio = self.destination_dimensions[1] / self.data.videoheight
+
+        if self.data.videoheight > self.data.videowidth:
+            # We're dealing with a portrait!
+            dimensions = (
+                int(self.data.videowidth * y_ratio),
+                180)
+        else:
+            dimensions = (
+                180,
+                int(self.data.videoheight * x_ratio))
+
+        return dimensions
+
+    def _on_message(self, bus, message):
+        _log.debug((bus, message))
+
+        t = message.type
+
+        if t == gst.MESSAGE_EOS:
+            self.__stop()
+            _log.info('Done')
+        elif t == gst.MESSAGE_ERROR:
+            _log.error((bus, message))
+            self.__stop()
+
+    def __stop(self):
         _log.debug(self.loop)
 
         self.pipeline.set_state(gst.STATE_NULL)
@@ -111,5 +358,42 @@ class VideoThumbnailer:
 
 
 if __name__ == '__main__':
-    VideoThumbnailer('/home/joar/Dropbox/Public/blender/fluid-box.mp4', '/tmp/dest.jpg')
-    VideoThumbnailer('/home/joar/Dropbox/iPhone/Video 2011-10-05 21 58 03.mov', '/tmp/dest2.jpg')
+    from optparse import OptionParser
+
+    parser = OptionParser(
+        usage='%prog [-v] -a [ video | thumbnail ] SRC DEST')
+
+    parser.add_option('-a', '--action',
+                      dest='action',
+                      help='One of "video" or "thumbnail"')
+
+    parser.add_option('-v',
+                      dest='verbose',
+                      action='store_true',
+                      help='Output debug information')
+
+    parser.add_option('-q',
+                      dest='quiet',
+                      action='store_true',
+                      help='Dear program, please be quiet unless *error*')
+
+    (options, args) = parser.parse_args()
+
+    if options.verbose:
+        _log.setLevel(logging.DEBUG)
+    else:
+        _log.setLevel(logging.INFO)
+
+    if options.quiet:
+        _log.setLevel(logging.ERROR)
+
+    _log.debug(args)
+
+    if not len(args) == 2:
+        parser.print_help()
+        sys.exit()
+
+    if options.action == 'thumbnail':
+        VideoThumbnailer(*args)
+    elif options.action == 'video':
+        VideoTranscoder(*args)