From a249b6d3a2e50a1cabd76a240ee391d9e54b1fbf Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Tue, 11 Oct 2011 04:57:17 +0200 Subject: [PATCH] - Refractored the video thumbnailer - Started work on video transcoder Not done, by far! - Bug fix in video.processing error handling --- mediagoblin/media_types/video/processing.py | 1 - mediagoblin/media_types/video/transcoders.py | 322 +++++++++++++++++-- 2 files changed, 303 insertions(+), 20 deletions(-) diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index d7a48caa..52047ae4 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -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): diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py index 1134bc66..d305d5fc 100644 --- a/mediagoblin/media_types/video/transcoders.py +++ b/mediagoblin/media_types/video/transcoders.py @@ -14,15 +14,18 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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) -- 2.25.1