Added new thumbnailer, renamed old.
authorJoar Wandborg <git@wandborg.com>
Thu, 27 Oct 2011 00:55:12 +0000 (02:55 +0200)
committerJoar Wandborg <git@wandborg.com>
Thu, 27 Oct 2011 00:55:12 +0000 (02:55 +0200)
mediagoblin/media_types/video/processing.py
mediagoblin/media_types/video/transcoders.py

index 09f8a0d91888c1b782e910937a1e1ce07a829c77..027f527bfc13e18537b668fe255803c2d4b1c573 100644 (file)
@@ -116,118 +116,6 @@ def process_video(entry):
 
     # Save the MediaEntry
     entry.save()
-    
-
-def __create_thumbnail(info):
-    thumbnail = tempfile.NamedTemporaryFile()
-
-    logger.info('thumbnailing...')
-    transcoders.VideoThumbnailer(info['tmp_file'].name, thumbnail.name)
-    logger.debug('Done thumbnailing')
-
-    os.remove(info['tmp_file'].name)
-
-    mgg.public_store.get_file(info['thumb_filepath'], 'wb').write(
-        thumbnail.read())
-
-
-    info['entry']['media_files']['thumb'] = info['thumb_filepath']
-    info['entry'].save()
-
-
-def __close_processing(queue, qentry, info, **kwargs):
-    '''
-    Updates MediaEntry, moves files, handles errors
-    '''
-    if not kwargs.get('error'):
-        logger.info('Transcoding successful')
-
-        qentry.transcoder.stop()
-        gobject.idle_add(info['loop'].quit)
-        info['loop'].quit()  # Do I have to do this again?
-
-        logger.info('Saving files...')
-
-        # Write the transcoded media to the storage system
-        with info['tmp_file'] as tmp_file:
-            mgg.public_store.get_file(info['medium_filepath'], 'wb').write(
-                tmp_file.read())
-            info['entry']['media_files']['medium'] = info['medium_filepath']
-
-        # we have to re-read because unlike PIL, not everything reads
-        # things in string representation :)
-        queued_file = file(info['queued_filename'], 'rb')
-
-        with queued_file:
-            original_filepath = create_pub_filepath(
-                info['entry'],
-                info['queued_filepath'][-1])
-
-            with mgg.public_store.get_file(original_filepath, 'wb') as \
-                    original_file:
-                original_file.write(queued_file.read())
-
-        mgg.queue_store.delete_file(info['queued_filepath'])
-
-
-        logger.debug('...Done')
-
-        info['entry']['queued_media_file'] = []
-        media_files_dict = info['entry'].setdefault('media_files', {})
-        media_files_dict['original'] = original_filepath
-
-        info['entry']['state'] = u'processed'
-        info['entry']['media_data'][u'preset'] = info['preset'].name
-        __create_thumbnail(info)
-        info['entry'].save()
-
-    else:
-        qentry.transcoder.stop()
-        gobject.idle_add(info['loop'].quit)
-        info['loop'].quit()
-        info['entry']['state'] = u'failed'
-        info['entry'].save()
-
-    # clean up workbench
-    info['workbench'].destroy_self()
-
-
-def _transcoding_start(queue, qentry, info):
-    logger.info('-> Starting transcoding')
-    logger.debug((queue, qentry, info))
-
-
-def _transcoding_complete(*args):
-    __close_processing(*args)
-    logger.debug(*args)
-
-
-def _transcoding_error(queue, qentry, arg, info):
-    logger.info('Error')
-    __close_processing(queue, qentry, info, error=True)
-
-
-def _transcoding_pass_setup(queue, qentry, options):
-    logger.info('Pass setup')
-    logger.debug((queue, qentry, options))
-
-
-def check_interrupted():
-    """
-        Check whether we have been interrupted by Ctrl-C and stop the
-        transcoder.
-    """
-    if interrupted:
-        try:
-            source = transcoder.pipe.get_by_name("source")
-            source.send_event(gst.event_new_eos())
-        except:
-            # Something pretty bad happened... just exit!
-            gobject.idle_add(loop.quit)
-
-        return False
-    return True
-
 
 def create_pub_filepath(entry, filename):
     return mgg.public_store.get_unique_filepath(
index 8115bb385b063c0112a1e97b5df0b59fdbf8aaaf..dfb0899efa70d20de062ceb6c139fd3326ec0a30 100644 (file)
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import division
+
+import os
+os.environ["GST_DEBUG_DUMP_DOT_DIR"] = "/tmp"
+os.putenv('GST_DEBUG_DUMP_DOT_DIR', '/tmp')
+
 import sys
 import logging
 import pdb
+import urllib
 
 _log = logging.getLogger(__name__)
 logging.basicConfig()
 _log.setLevel(logging.DEBUG)
 
+import gtk
+
 try:
     import gobject
     gobject.threads_init()
 except:
-    _log.error('Could not import gobject')
-    raise Exception()
+    raise Exception('gobject could not be found')
 
 try:
     import pygst
@@ -37,11 +44,288 @@ try:
     from gst import pbutils
     from gst.extend import discoverer
 except:
-    _log.error('pygst could not be imported')
-    raise Exception()
+    raise Exception('gst/pygst 0.10 could not be found')
 
 
 class VideoThumbnailer:
+    # Declaration of thumbnailer states
+    STATE_NULL = 0
+    STATE_HALTING = 1
+    STATE_PROCESSING = 2
+
+    # The current thumbnailer state
+    state = STATE_NULL
+
+    # This will contain the thumbnailing pipeline
+    thumbnail_pipeline = None
+
+    buffer_probes = {}
+
+    errors = []
+
+    def __init__(self, source_path, dest_path):
+        '''
+        Set up playbin pipeline in order to get video properties.
+
+        Initializes and runs the gobject.MainLoop()
+        '''
+        self.source_path = source_path
+        self.dest_path = dest_path
+
+        self.loop = gobject.MainLoop()
+
+        # Set up the playbin. It will be used to discover certain
+        # properties of the input file
+        self.playbin = gst.element_factory_make('playbin')
+
+        self.videosink = gst.element_factory_make('fakesink', 'videosink')
+        self.playbin.set_property('video-sink', self.videosink)
+
+        #self.audiosink = gst.element_factory_make('fakesink', 'audiosink')
+        #self.playbin.set_property('audio-sink', self.audiosink)
+
+        self.bus = self.playbin.get_bus()
+        self.bus.add_signal_watch()
+        self.watch_id = self.bus.connect('message', self._on_bus_message)
+
+        self.playbin.set_property('uri', 'file:{0}'.format(
+                urllib.pathname2url(self.source_path)))
+
+        self.playbin.set_state(gst.STATE_PAUSED)
+
+        self.run()
+
+    def run(self):
+        self.loop.run()
+
+    def _on_bus_message(self, bus, message):
+        _log.debug(' BUS MESSAGE: {0}'.format(message))
+
+        if message.type == gst.MESSAGE_ERROR:
+            gobject.idle_add(self._on_bus_error)
+
+        elif message.type == gst.MESSAGE_STATE_CHANGED:
+            # The pipeline state has changed
+            # Parse state changing data
+            _prev, state, _pending = message.parse_state_changed()
+
+            _log.debug('State changed: {0}'.format(state))
+
+            if state == gst.STATE_PAUSED:
+                if message.src == self.playbin:
+                    gobject.idle_add(self._on_bus_paused)
+
+    def _on_bus_paused(self):
+        '''
+        Set up thumbnailing pipeline
+        '''
+        current_video = self.playbin.get_property('current-video')
+
+        if current_video == 0:
+            _log.debug('Found current video from playbin')
+        else:
+            _log.error('Could not get any current video from playbin!')
+
+        self.duration = self._get_duration(self.playbin)
+        _log.info('Video length: {0}'.format(self.duration / gst.SECOND))
+
+        _log.info('Setting up thumbnailing pipeline')
+        self.thumbnail_pipeline = gst.parse_launch(
+            'filesrc location="{0}" ! decodebin ! '
+            'ffmpegcolorspace ! videoscale ! '
+            'video/x-raw-rgb,depth=24,bpp=24,pixel-aspect-ratio=1/1,width=180 ! '
+            'fakesink signal-handoffs=True'.format(self.source_path))
+
+        self.thumbnail_bus = self.thumbnail_pipeline.get_bus()
+        self.thumbnail_bus.add_signal_watch()
+        self.thumbnail_watch_id = self.thumbnail_bus.connect(
+            'message', self._on_thumbnail_bus_message)
+
+        self.thumbnail_pipeline.set_state(gst.STATE_PAUSED)
+
+        #gobject.timeout_add(3000, self._on_timeout)
+
+        return False
+
+    def _on_thumbnail_bus_message(self, bus, message):
+        _log.debug('Thumbnail bus called, message: {0}'.format(message))
+
+        if message.type == gst.MESSAGE_ERROR:
+            _log.error(message)
+            gobject.idle_add(self._on_bus_error)
+
+        if message.type == gst.MESSAGE_STATE_CHANGED:
+            _prev, state, _pending = message.parse_state_changed()
+
+            if (state == gst.STATE_PAUSED and
+                not self.state == self.STATE_PROCESSING and
+                message.src == self.thumbnail_pipeline):
+                _log.info('Pipeline paused, processing')
+                self.state = self.STATE_PROCESSING
+
+                for sink in self.thumbnail_pipeline.sinks():
+                    name = sink.get_name()
+                    factoryname = sink.get_factory().get_name()
+
+                    if factoryname == 'fakesink':
+                        sinkpad = sink.get_pad('sink')
+
+                        self.buffer_probes[name] = sinkpad.add_buffer_probe(
+                            self.buffer_probe_handler, name)
+
+                        _log.info('Added buffer probe')
+
+                        break
+
+                # Apply the wadsworth constant, fallback to 1 second
+                seek_amount = max(self.duration / 100 * 30, 1 * gst.SECOND)
+
+                _log.debug('seek amount: {0}'.format(seek_amount))
+
+                
+                seek_result = self.thumbnail_pipeline.seek(
+                    1.0,
+                    gst.FORMAT_TIME,
+                    gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE,
+                    gst.SEEK_TYPE_SET,
+                    seek_amount,
+                    gst.SEEK_TYPE_NONE,
+                    0)
+                '''
+
+                seek_result = self.thumbnail_pipeline.seek_simple(
+                    gst.FORMAT_TIME,
+                    gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE,
+                    seek_amount)
+
+                '''
+
+                if not seek_result:
+                    self.errors.append('COULD_NOT_SEEK')
+                    _log.error('Couldn\'t seek! result: {0}'.format(
+                            seek_result))
+                    _log.info(message)
+                    self.shutdown()
+                else:
+                    pass
+                    #self.thumbnail_pipeline.set_state(gst.STATE_PAUSED)
+                    #pdb.set_trace()
+
+    def buffer_probe_handler_real(self, pad, buff, name):
+        '''
+        Capture buffers as gdk_pixbufs when told to.
+        '''
+        try:
+            caps = buff.caps
+            if caps is None:
+                _log.error('No caps passed to buffer probe handler!')
+                self.shutdown()
+                return False
+
+            _log.debug('caps: {0}'.format(caps))
+
+            filters = caps[0]
+            width = filters["width"]
+            height = filters["height"]
+
+            pixbuf = gtk.gdk.pixbuf_new_from_data(
+                buff.data, gtk.gdk.COLORSPACE_RGB, False, 8,
+                width, height, width * 3)
+
+            # NOTE: 200x136 is sort of arbitrary.  it's larger than what
+            # the ui uses at the time of this writing.
+            # new_width, new_height = scaled_size((width, height), (200, 136))
+
+            #pixbuf = pixbuf.scale_simple(
+            #new_width, new_height, gtk.gdk.INTERP_BILINEAR)
+
+            pixbuf.save(self.dest_path, 'jpeg')
+            _log.info('Saved thumbnail')
+            del pixbuf
+            self.shutdown()
+        except gst.QueryError:
+            pass
+        return False
+
+    def buffer_probe_handler(self, pad, buff, name):
+        '''
+        Proxy function for buffer_probe_handler_real
+        '''
+        gobject.idle_add(
+            lambda: self.buffer_probe_handler_real(pad, buff, name))
+
+        return True
+
+    def _get_duration(self, pipeline, retries=0):
+        '''
+        Get the duration of a pipeline.
+
+        Retries 5 times.
+        '''
+        if retries == 5:
+            return 0
+
+        try:
+            return pipeline.query_duration(gst.FORMAT_TIME)[0] 
+        except gst.QueryError:
+            return self._get_duration(pipeline, retries + 1)
+
+    def _on_timeout(self):
+        _log.error('TIMEOUT! DROP EVERYTHING!')
+        self.shutdown()
+
+    def _on_bus_error(self, *args):
+        _log.error('AHAHAHA! Error! args: {0}'.format(args))
+
+    def shutdown(self):
+        '''
+        Tell gobject to call __halt when the mainloop is idle.
+        '''
+        _log.info('Shutting down')
+        self.__halt()
+
+    def __halt(self):
+        '''
+        Halt all pipelines and shut down the main loop
+        '''
+        _log.info('Halting...')
+        self.state = self.STATE_HALTING
+
+        self.__disconnect()
+
+        gobject.idle_add(self.__halt_final)
+
+    def __disconnect(self):
+        _log.debug('Disconnecting...')
+        if not self.playbin is None:
+            self.playbin.set_state(gst.STATE_NULL)
+            for sink in self.playbin.sinks():
+                name = sink.get_name()
+                factoryname = sink.get_factory().get_name()
+
+                _log.debug('Disconnecting {0}'.format(name))
+
+                if factoryname == "fakesink":
+                    pad = sink.get_pad("sink")
+                    pad.remove_buffer_probe(self.buffer_probes[name])
+                    del self.buffer_probes[name]
+
+        self.playbin = None
+
+        if self.bus is not None:
+            self.bus.disconnect(self.watch_id)
+            self.bus = None
+
+
+    def __halt_final(self):
+        _log.info('Done')
+        if self.errors:
+            _log.error(','.join(self.errors))
+            
+        self.loop.quit()
+
+
+class DeprecatedVideoThumbnailer:
     '''
     Creates a video thumbnail
 
@@ -50,14 +334,18 @@ class VideoThumbnailer:
      - 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
+     - 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
     '''
+
+    WADSWORTH_CONSTANT = 30 # percent
+
     def __init__(self, src, dst, **kwargs):
         _log.info('Initializing VideoThumbnailer...')
 
         self.loop = gobject.MainLoop()
+
         self.source_path = src
         self.destination_path = dst
 
@@ -70,7 +358,7 @@ class VideoThumbnailer:
         self._run()
 
     def _setup(self):
-        self._setup_pass()
+        self._setup_pipeline()
         self._setup_discover()
 
     def _run(self):
@@ -99,28 +387,30 @@ class VideoThumbnailer:
 
         self.data = data
 
+        # Run any tasks that depend on the info from the discovery
         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):
+    def _setup_pipeline(self):
+        # Create a new pipeline
         self.pipeline = gst.Pipeline('VideoThumbnailerPipeline')
 
+        # Create the elements in the pipeline
         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.ffmpegcolorspace = gst.element_factory_make(
+            'ffmpegcolorspace', 'ffmpegcolorspace')
         self.pipeline.add(self.ffmpegcolorspace)
 
         self.videoscale = gst.element_factory_make('videoscale', 'videoscale')
@@ -132,70 +422,113 @@ class VideoThumbnailer:
 
         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', self.destination_path)
+        #self.pipeline.add(self.filesink)
 
-        self.filesink = gst.element_factory_make('filesink', 'filesink')
-        self.filesink.set_property('location', self.destination_path)
-        self.pipeline.add(self.filesink)
+        self.appsink = gst.element_factory_make('appsink', 'appsink')
+        self.appsink.set_property('emit-signals', True)
+        self.appsink.connect('new-preroll', self.__on_sink_preroll)
+        self.pipeline.add(self.appsink)
+
+        self.progressreport = gst.element_factory_make(
+            'progressreport', 'progressreport')
+        self.progressreport.set_property('update-freq', 1)
+        self.pipeline.add(self.progressreport)
+
+        self.identity = gst.element_factory_make('identity', 'id')
+        self.pipeline.add(self.identity)
 
         # Link all the elements together
         self.filesrc.link(self.decoder)
         self.ffmpegcolorspace.link(self.videoscale)
         self.videoscale.link(self.capsfilter)
         self.capsfilter.link(self.jpegenc)
-        self.jpegenc.link(self.filesink)
+        self.jpegenc.link(self.progressreport)
+        self.progressreport.link(self.identity)
+        #self.identity.link(self.filesink)
+        self.identity.link(self.appsink)
+
+        self.pipeline.set_state(gst.STATE_PAUSED)
 
         self._setup_bus()
 
+    def __on_sink_preroll(self, sink):
+        _log.debug('SINK PREROLL!!!!')
+
+    def _on_dynamic_pad(self, dbin, pad, islast):
+        '''
+        Callback called when ``decodebin2`` has a pad that we can connect to
+        '''
+        # Intersect the capabilities of the video sink and the pad src
+        # Then check if they have no common capabilities.
+        if not self.ffmpegcolorspace.get_pad_template('sink')\
+                .get_caps().intersect(pad.get_caps()).is_empty():
+            # It IS a video src pad.
+            pad.link(self.ffmpegcolorspace.get_pad('sink'))
+            gst.DEBUG_BIN_TO_DOT_FILE(
+                self.pipeline,
+                gst.DEBUG_GRAPH_SHOW_ALL,
+                'ss')
+
     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)
+        caps = ['video/x-raw-rgb', 'pixel-aspect-ratio=1/1']
+
+        if self.data.videoheight > self.data.videowidth:
+            # Whoa! We have ourselves a portrait video!
+            caps.append('height={0}'.format(
+                    self.destination_dimensions[1]))
+        else:
+            # It's a landscape, phew, how normal.
+            caps.append('width={0}'.format(
+                    self.destination_dimensions[0]))
 
         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
+            gst.caps_from_string(
+                ', '.join(caps)))
 
-        if self.data.videoheight > self.data.videowidth:
-            # We're dealing with a portrait!
-            dimensions = (
-                int(self.data.videowidth * y_ratio),
-                180)
+    def __find_wadsworth(self):
+        if self.decoder.seek_simple(
+            gst.FORMAT_PERCENT,
+            gst.SEEK_FLAG_NONE,
+            0 * 10000):
+            _log.info('Found wadsworth')
+            #pdb.set_trace()
+            #self.pipeline.set_state(gst.STATE_PLAYING)
+            self.__get_buffer()
+            self.__stop()
         else:
-            dimensions = (
-                180,
-                int(self.data.videoheight * x_ratio))
+            pdb.set_trace()
 
-        return dimensions
+    def __get_buffer(self):
+        buffer = self.appsink.emit('pull-preroll')
+        open(self.destination_path, 'wb').write(buffer)
 
     def _on_message(self, bus, message):
-        _log.debug((bus, message))
-
         t = message.type
 
+        _log.debug((
+                t == gst.MESSAGE_ASYNC_DONE,
+                bus,
+                message))
+
         if t == gst.MESSAGE_EOS:
             self.__stop()
-            _log.info('Done')
+            _log.info('Got EOS')
+        elif t == gst.MESSAGE_ASYNC_DONE:
+            #pdb.set_trace()
+            self.__find_wadsworth()
         elif t == gst.MESSAGE_ERROR:
             _log.error((bus, message))
             self.__stop()
 
-    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 __stop(self):
         _log.debug(self.loop)
 
@@ -210,8 +543,11 @@ class VideoTranscoder:
 
     Transcodes the SRC video file to a VP8 WebM video file at DST
 
-    TODO:
-    - Audio pipeline
+     - Does the same thing as VideoThumbnailer, but produces a WebM vp8
+       and vorbis video file.
+     - The VideoTranscoder exceeds the VideoThumbnailer in the way
+       that it was refined afterwards and therefore is done more
+       correctly.
     '''
     def __init__(self, src, dst, **kwargs):
         _log.info('Initializing VideoTranscoder...')
@@ -220,7 +556,9 @@ class VideoTranscoder:
         self.source_path = src
         self.destination_path = dst
 
+        # Options
         self.destination_dimensions = kwargs.get('dimensions') or (640, 640)
+        self._progress_callback = kwargs.get('progress_callback') or None
 
         if not type(self.destination_dimensions) == tuple:
             raise Exception('dimensions must be tuple: (width, height)')
@@ -229,8 +567,8 @@ class VideoTranscoder:
         self._run()
 
     def _setup(self):
-        self._setup_pass()
         self._setup_discover()
+        self._setup_pipeline()
 
     def _run(self):
         _log.info('Discovering...')
@@ -241,6 +579,7 @@ class VideoTranscoder:
         self.loop.run()
 
     def _setup_discover(self):
+        _log.debug('Setting up discoverer')
         self.discoverer = discoverer.Discoverer(self.source_path)
 
         # Connect self.__discovered to the 'discovered' event
@@ -254,32 +593,36 @@ class VideoTranscoder:
             self.__stop()
             raise Exception('Could not discover {0}'.format(self.source_path))
 
-        _log.debug('__discovered, data: {0}'.format(data))
+        _log.debug('__discovered, data: {0}'.format(data.__dict__))
 
         self.data = data
 
-        self._on_discovered()
+        # Launch things that should be done after discovery
+        self._link_elements()
+        self.__setup_videoscale_capsfilter()
 
         # Tell the transcoding pipeline to start running
         self.pipeline.set_state(gst.STATE_PLAYING)
         _log.info('Transcoding...')
 
-    def _on_discovered(self):
-        self.__setup_videoscale_capsfilter()
-
-    def _setup_pass(self):
+    def _setup_pipeline(self):
+        _log.debug('Setting up transcoding pipeline')
+        # Create the pipeline bin.
         self.pipeline = gst.Pipeline('VideoTranscoderPipeline')
 
+        # Create all GStreamer elements, starting with
+        # filesrc & decoder
         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')
+        # Video elements
+        self.ffmpegcolorspace = gst.element_factory_make(
+            'ffmpegcolorspace', 'ffmpegcolorspace')
         self.pipeline.add(self.ffmpegcolorspace)
 
         self.videoscale = gst.element_factory_make('videoscale', 'videoscale')
@@ -293,11 +636,9 @@ class VideoTranscoder:
         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.pipeline.add(self.vp8enc)
 
-
-        # Audio
+        # Audio elements
         self.audioconvert = gst.element_factory_make('audioconvert', 'audioconvert')
         self.pipeline.add(self.audioconvert)
 
@@ -305,7 +646,7 @@ class VideoTranscoder:
         self.vorbisenc.set_property('quality', 0.7)
         self.pipeline.add(self.vorbisenc)
 
-
+        # WebMmux & filesink
         self.webmmux = gst.element_factory_make('webmmux', 'webmmux')
         self.pipeline.add(self.webmmux)
 
@@ -313,35 +654,60 @@ class VideoTranscoder:
         self.filesink.set_property('location', self.destination_path)
         self.pipeline.add(self.filesink)
 
+        # Progressreport
+        self.progressreport = gst.element_factory_make(
+            'progressreport', 'progressreport')
+        # Update every second
+        self.progressreport.set_property('update-freq', 1)
+        self.progressreport.set_property('silent', True)
+        self.pipeline.add(self.progressreport)
+
+    def _link_elements(self):
+        '''
+        Link all the elements
+
+        This code depends on data from the discoverer and is called
+        from __discovered
+        '''
+        _log.debug('linking elements')
+        # Link the filesrc element to the decoder. The decoder then emits
+        # 'new-decoded-pad' which links decoded src pads to either a video
+        # or audio sink
         self.filesrc.link(self.decoder)
+
+        # Link all the video elements in a link to webmux
         self.ffmpegcolorspace.link(self.videoscale)
         self.videoscale.link(self.capsfilter)
+        #self.capsfilter.link(self.xvimagesink)
         self.capsfilter.link(self.vp8enc)
         self.vp8enc.link(self.webmmux)
 
-        # Audio
-        self.audioconvert.link(self.vorbisenc)
-        self.vorbisenc.link(self.webmmux)
+        if self.data.is_audio:
+            # Link all the audio elements in a line to webmux
+            #self.audioconvert.link(self.alsasink)
+            self.audioconvert.link(self.vorbisenc)
+            self.vorbisenc.link(self.webmmux)
 
-        self.webmmux.link(self.filesink)
+        self.webmmux.link(self.progressreport)
+        self.progressreport.link(self.filesink)
 
+        # Setup the message bus and connect _on_message to the pipeline
         self._setup_bus()
 
+
     def _on_dynamic_pad(self, dbin, pad, islast):
         '''
         Callback called when ``decodebin2`` has a pad that we can connect to
         '''
-        _log.debug('Linked {0}'.format(pad))
-
-        #pdb.set_trace()
-
+        # Intersect the capabilities of the video sink and the pad src
+        # Then check if they have no common capabilities.
         if self.ffmpegcolorspace.get_pad_template('sink')\
                 .get_caps().intersect(pad.get_caps()).is_empty():
-            pad.link(
-                self.audioconvert.get_pad('sink'))
+            # It is NOT a video src pad.
+            pad.link(self.audioconvert.get_pad('sink'))
         else:
-            pad.link(
-                self.ffmpegcolorspace.get_pad('sink'))
+            # It IS a video src pad.
+            pad.link(self.ffmpegcolorspace.get_pad('sink'))
 
     def _setup_bus(self):
         self.bus = self.pipeline.get_bus()
@@ -349,6 +715,9 @@ class VideoTranscoder:
         self.bus.connect('message', self._on_message)
 
     def __setup_videoscale_capsfilter(self):
+        '''
+        Sets up the output format (width, height) for the video
+        '''
         caps = ['video/x-raw-yuv', 'pixel-aspect-ratio=1/1']
 
         if self.data.videoheight > self.data.videowidth:
@@ -364,19 +733,31 @@ class VideoTranscoder:
             'caps',
             gst.caps_from_string(
                 ', '.join(caps)))
-        gst.DEBUG_BIN_TO_DOT_FILE (
-            self.pipeline,
-            gst.DEBUG_GRAPH_SHOW_ALL,
-            'supersimple-debug-graph')
 
     def _on_message(self, bus, message):
-        _log.debug((bus, message))
+        _log.debug((bus, message, message.type))
 
         t = message.type
 
         if t == gst.MESSAGE_EOS:
             self._discover_dst_and_stop()
             _log.info('Done')
+
+        elif t == gst.MESSAGE_ELEMENT:
+            if message.structure.get_name() == 'progress':
+                data = {
+                    'structure': message.structure,
+                    'percent': message.structure['percent'],
+                    'total': message.structure['total'],
+                    'current': message.structure['current']}
+
+                if self._progress_callback:
+                    self._progress_callback(data)
+
+                _log.info('{percent}% done...'.format(
+                        percent=data['percent']))
+                _log.debug(data)
+
         elif t == gst.MESSAGE_ERROR:
             _log.error((bus, message))
             self.__stop()
@@ -397,15 +778,25 @@ class VideoTranscoder:
     def __stop(self):
         _log.debug(self.loop)
 
+        # Stop executing the pipeline
         self.pipeline.set_state(gst.STATE_NULL)
 
-        gobject.idle_add(self.loop.quit)
+        # This kills the loop, mercifully
+        gobject.idle_add(self.__stop_mainloop)
+
+    def __stop_mainloop(self):
+        '''
+        Wrapper for gobject.MainLoop.quit()
+
+        This wrapper makes us able to see if self.loop.quit has been called
+        '''
+        _log.info('Terminating MainLoop')
+
+        self.loop.quit()
 
 
 if __name__ == '__main__':
-    import os
-    os.environ["GST_DEBUG_DUMP_DOT_DIR"] = "/tmp"
-    os.putenv('GST_DEBUG_DUMP_DOT_DIR', '/tmp')
+    os.nice(19)
     from optparse import OptionParser
 
     parser = OptionParser(
@@ -444,5 +835,6 @@ if __name__ == '__main__':
     if options.action == 'thumbnail':
         VideoThumbnailer(*args)
     elif options.action == 'video':
-        transcoder = VideoTranscoder(*args)
-        pdb.set_trace()
+        def cb(data):
+            print('I\'m a callback!')
+        transcoder = VideoTranscoder(*args, progress_callback=cb)