- Refractored the video thumbnailer
- Started work on video transcoder Not done, by far! - Bug fix in video.processing error handling
This commit is contained in:
parent
89d764cd70
commit
a249b6d3a2
@ -213,7 +213,6 @@ def _transcoding_complete(*args):
|
|||||||
def _transcoding_error(queue, qentry, arg, info):
|
def _transcoding_error(queue, qentry, arg, info):
|
||||||
logger.info('Error')
|
logger.info('Error')
|
||||||
__close_processing(queue, qentry, info, error=True)
|
__close_processing(queue, qentry, info, error=True)
|
||||||
logger.debug((queue, quentry, info, arg))
|
|
||||||
|
|
||||||
|
|
||||||
def _transcoding_pass_setup(queue, qentry, options):
|
def _transcoding_pass_setup(queue, qentry, options):
|
||||||
|
@ -14,15 +14,18 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
_log = logging.getLogger(__name__)
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
_log.setLevel(logging.INFO)
|
_log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import gobject
|
import gobject
|
||||||
|
gobject.threads_init()
|
||||||
except:
|
except:
|
||||||
_log.error('Could not import gobject')
|
_log.error('Could not import gobject')
|
||||||
|
|
||||||
@ -30,24 +33,83 @@ try:
|
|||||||
import pygst
|
import pygst
|
||||||
pygst.require('0.10')
|
pygst.require('0.10')
|
||||||
import gst
|
import gst
|
||||||
|
from gst.extend import discoverer
|
||||||
except:
|
except:
|
||||||
_log.error('pygst could not be imported')
|
_log.error('pygst could not be imported')
|
||||||
|
|
||||||
|
|
||||||
class VideoThumbnailer:
|
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.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()
|
self.loop.run()
|
||||||
|
|
||||||
def _set_up_pass(self, src, dst):
|
def _setup_discover(self):
|
||||||
self.pipeline = gst.Pipeline('TranscodingPipeline')
|
self.discoverer = discoverer.Discoverer(self.source_path)
|
||||||
|
|
||||||
_log.debug('Pipeline: {0}'.format(self.pipeline))
|
# 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
|
||||||
|
|
||||||
|
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 = 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.pipeline.add(self.filesrc)
|
||||||
|
|
||||||
self.decoder = gst.element_factory_make('decodebin2', 'decoder')
|
self.decoder = gst.element_factory_make('decodebin2', 'decoder')
|
||||||
@ -59,18 +121,17 @@ class VideoThumbnailer:
|
|||||||
self.pipeline.add(self.ffmpegcolorspace)
|
self.pipeline.add(self.ffmpegcolorspace)
|
||||||
|
|
||||||
self.videoscale = gst.element_factory_make('videoscale', 'videoscale')
|
self.videoscale = gst.element_factory_make('videoscale', 'videoscale')
|
||||||
|
self.videoscale.set_property('method', 'bilinear')
|
||||||
self.pipeline.add(self.videoscale)
|
self.pipeline.add(self.videoscale)
|
||||||
|
|
||||||
self.capsfilter = gst.element_factory_make('capsfilter', 'capsfilter')
|
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.pipeline.add(self.capsfilter)
|
||||||
|
|
||||||
self.jpegenc = gst.element_factory_make('jpegenc', 'jpegenc')
|
self.jpegenc = gst.element_factory_make('jpegenc', 'jpegenc')
|
||||||
self.pipeline.add(self.jpegenc)
|
self.pipeline.add(self.jpegenc)
|
||||||
|
|
||||||
self.filesink = gst.element_factory_make('filesink', 'filesink')
|
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)
|
self.pipeline.add(self.filesink)
|
||||||
|
|
||||||
# Link all the elements together
|
# Link all the elements together
|
||||||
@ -80,20 +141,50 @@ class VideoThumbnailer:
|
|||||||
self.capsfilter.link(self.jpegenc)
|
self.capsfilter.link(self.jpegenc)
|
||||||
self.jpegenc.link(self.filesink)
|
self.jpegenc.link(self.filesink)
|
||||||
|
|
||||||
bus = self.pipeline.get_bus()
|
self._setup_bus()
|
||||||
bus.add_signal_watch()
|
|
||||||
bus.connect('message', self._on_message)
|
|
||||||
|
|
||||||
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):
|
def _on_message(self, bus, message):
|
||||||
_log.info((bus, message))
|
_log.debug((bus, message))
|
||||||
|
|
||||||
t = message.type
|
t = message.type
|
||||||
|
|
||||||
if t == gst.MESSAGE_EOS:
|
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):
|
def _on_dynamic_pad(self, dbin, pad, islast):
|
||||||
'''
|
'''
|
||||||
@ -102,7 +193,163 @@ class VideoThumbnailer:
|
|||||||
pad.link(
|
pad.link(
|
||||||
self.ffmpegcolorspace.get_pad('sink'))
|
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)
|
_log.debug(self.loop)
|
||||||
|
|
||||||
self.pipeline.set_state(gst.STATE_NULL)
|
self.pipeline.set_state(gst.STATE_NULL)
|
||||||
@ -111,5 +358,42 @@ class VideoThumbnailer:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
VideoThumbnailer('/home/joar/Dropbox/Public/blender/fluid-box.mp4', '/tmp/dest.jpg')
|
from optparse import OptionParser
|
||||||
VideoThumbnailer('/home/joar/Dropbox/iPhone/Video 2011-10-05 21 58 03.mov', '/tmp/dest2.jpg')
|
|
||||||
|
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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user