Added VideoThumbnailerMarkII
- Set video.processing to use VideoThumbnailerMarkII.
This commit is contained in:
parent
3d7fa496ee
commit
e4a1b6d22a
@ -104,7 +104,10 @@ def process_video(entry):
|
||||
|
||||
with tmp_thumb:
|
||||
# Create a thumbnail.jpg that fits in a 180x180 square
|
||||
transcoders.VideoThumbnailer(queued_filename, tmp_thumb.name)
|
||||
transcoders.VideoThumbnailerMarkII(
|
||||
queued_filename,
|
||||
tmp_thumb.name,
|
||||
180)
|
||||
|
||||
# Push the thumbnail to public storage
|
||||
_log.debug('Saving thumbnail...')
|
||||
|
@ -60,12 +60,6 @@ class VideoThumbnailer:
|
||||
STATE_PROCESSING = 2
|
||||
|
||||
# The current thumbnailer state
|
||||
state = STATE_NULL
|
||||
|
||||
# This will contain the thumbnailing pipeline
|
||||
thumbnail_pipeline = None
|
||||
|
||||
buffer_probes = {}
|
||||
|
||||
def __init__(self, source_path, dest_path):
|
||||
'''
|
||||
@ -78,6 +72,10 @@ class VideoThumbnailer:
|
||||
into the playbin
|
||||
- Initialize
|
||||
'''
|
||||
# This will contain the thumbnailing pipeline
|
||||
self.state = self.STATE_NULL
|
||||
self.thumbnail_pipeline = None
|
||||
self.buffer_probes = {}
|
||||
self.errors = []
|
||||
|
||||
self.source_path = source_path
|
||||
@ -332,6 +330,298 @@ class VideoThumbnailer:
|
||||
self.loop.quit()
|
||||
|
||||
|
||||
class VideoThumbnailerMarkII(object):
|
||||
'''
|
||||
Creates a thumbnail from a video file. Rewrite of VideoThumbnailer.
|
||||
|
||||
Large parts of the functionality and overall architectue contained within
|
||||
this object is taken from Participatory Culture Foundation's
|
||||
`gst_extractor.Extractor` object last seen at
|
||||
https://github.com/pculture/miro/blob/master/tv/lib/frontends/widgets/gst/gst_extractor.py
|
||||
in the `miro` codebase.
|
||||
|
||||
The `miro` codebase and the gst_extractor.py are licensed under the GNU
|
||||
General Public License v2 or later.
|
||||
'''
|
||||
STATE_NULL = 0
|
||||
STATE_HALTING = 1
|
||||
STATE_PROCESSING = 2
|
||||
STATE_PROCESSING_THUMBNAIL = 3
|
||||
|
||||
def __init__(self, source_path, dest_path, width=None, height=None,
|
||||
position_callback=None):
|
||||
self.state = self.STATE_NULL
|
||||
|
||||
self.has_reached_playbin_pause = False
|
||||
|
||||
self.thumbnail_pipeline = None
|
||||
|
||||
self.permission_to_take_picture = False
|
||||
|
||||
self.buffer_probes = {}
|
||||
|
||||
self.errors = []
|
||||
|
||||
self.source_path = os.path.abspath(source_path)
|
||||
self.dest_path = os.path.abspath(dest_path)
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.position_callback = position_callback \
|
||||
or self.wadsworth_position_callback
|
||||
|
||||
self.mainloop = gobject.MainLoop()
|
||||
|
||||
self.playbin = gst.element_factory_make('playbin')
|
||||
|
||||
self.videosink = gst.element_factory_make('fakesink', 'videosink')
|
||||
self.audiosink = gst.element_factory_make('fakesink', 'audiosink')
|
||||
|
||||
self.playbin.set_property('video-sink', self.videosink)
|
||||
self.playbin.set_property('audio-sink', self.audiosink)
|
||||
|
||||
self.playbin_message_bus = self.playbin.get_bus()
|
||||
|
||||
self.playbin_message_bus.add_signal_watch()
|
||||
self.playbin_bus_watch_id = self.playbin_message_bus.connect(
|
||||
'message',
|
||||
self.on_playbin_message)
|
||||
|
||||
self.playbin.set_property(
|
||||
'uri',
|
||||
'file:{0}'.format(
|
||||
urllib.pathname2url(self.source_path)))
|
||||
|
||||
self.playbin.set_state(gst.STATE_PAUSED)
|
||||
|
||||
try:
|
||||
self.run()
|
||||
except Exception as exc:
|
||||
_log.critical(
|
||||
'Exception "{0}" caught, disconnecting and re-raising'\
|
||||
.format(exc))
|
||||
self.disconnect()
|
||||
raise
|
||||
|
||||
def wadsworth_position_callback(self, duration, gst):
|
||||
return self.duration / 100 * 30
|
||||
|
||||
def run(self):
|
||||
self.mainloop.run()
|
||||
|
||||
def on_playbin_message(self, message_bus, message):
|
||||
_log.debug('playbin message: {0}'.format(message))
|
||||
|
||||
if message.type == gst.MESSAGE_ERROR:
|
||||
_log.error('playbin error: {0}'.format(message))
|
||||
gobject.idle_add(self.on_playbin_error)
|
||||
|
||||
if message.type == gst.MESSAGE_STATE_CHANGED:
|
||||
prev_state, cur_state, pending_state = \
|
||||
message.parse_state_changed()
|
||||
|
||||
_log.debug('playbin state changed: \nprev: {0}\ncur: {1}\n \
|
||||
pending: {2}'.format(
|
||||
prev_state,
|
||||
cur_state,
|
||||
pending_state))
|
||||
|
||||
if cur_state == gst.STATE_PAUSED:
|
||||
if message.src == self.playbin:
|
||||
_log.info('playbin ready')
|
||||
gobject.idle_add(self.on_playbin_paused)
|
||||
|
||||
def on_playbin_paused(self):
|
||||
if self.has_reached_playbin_pause:
|
||||
_log.warn('Has already reached logic for playbin pause. Aborting \
|
||||
without doing anything this time.')
|
||||
return False
|
||||
|
||||
self.has_reached_playbin_pause = True
|
||||
|
||||
current_video = self.playbin.get_property('current-video')
|
||||
|
||||
if not current_video:
|
||||
_log.critical('thumbnail could not get any video data \
|
||||
from playbin')
|
||||
|
||||
self.duration = self.get_duration(self.playbin)
|
||||
self.permission_to_take_picture = True
|
||||
self.buffer_probes = {}
|
||||
|
||||
pipeline = ''.join([
|
||||
'filesrc location="%s" ! decodebin ! ' % self.source_path,
|
||||
'ffmpegcolorspace ! videoscale ! ',
|
||||
'video/x-raw-rgb,depth=24,bpp=24,pixel-aspect-ratio=1/1',
|
||||
',width={0}'.format(self.width) if self.width else '',
|
||||
',height={0}'.format(self.height) if self.height else '',
|
||||
' ! ',
|
||||
'fakesink signal-handoffs=True'])
|
||||
|
||||
_log.debug('thumbnail_pipeline: {0}'.format(pipeline))
|
||||
|
||||
self.thumbnail_pipeline = gst.parse_launch(pipeline)
|
||||
self.thumbnail_message_bus = self.thumbnail_pipeline.get_bus()
|
||||
self.thumbnail_message_bus.add_signal_watch()
|
||||
self.thumbnail_bus_watch_id = self.thumbnail_message_bus.connect(
|
||||
'message',
|
||||
self.on_thumbnail_message)
|
||||
|
||||
self.thumbnail_pipeline.set_state(gst.STATE_PAUSED)
|
||||
|
||||
gobject.timeout_add(3000, self.on_gobject_timeout)
|
||||
|
||||
return False
|
||||
|
||||
def on_thumbnail_message(self, message_bus, message):
|
||||
_log.debug('thumbnail message: {0}'.format(message))
|
||||
|
||||
if message.type == gst.MESSAGE_ERROR:
|
||||
_log.error('thumbnail error: {0}'.format(message))
|
||||
gobject.idle_add(self.on_thumbnail_error)
|
||||
|
||||
if message.type == gst.MESSAGE_STATE_CHANGED:
|
||||
prev_state, cur_state, pending_state = \
|
||||
message.parse_state_changed()
|
||||
|
||||
_log.debug('thumbnail state changed: \nprev: {0}\ncur: {1}\n \
|
||||
pending: {2}'.format(
|
||||
prev_state,
|
||||
cur_state,
|
||||
pending_state))
|
||||
|
||||
if cur_state == gst.STATE_PAUSED and\
|
||||
not self.state == self.STATE_PROCESSING_THUMBNAIL:
|
||||
self.state = self.STATE_PROCESSING_THUMBNAIL
|
||||
|
||||
# Find the fakesink sink pad and attach the on_buffer_probe
|
||||
# handler to it.
|
||||
for sink in self.thumbnail_pipeline.sinks():
|
||||
sink_name = sink.get_name()
|
||||
sink_factory_name = sink.get_factory().get_name()
|
||||
|
||||
if sink_factory_name == 'fakesink':
|
||||
sink_pad = sink.get_pad('sink')
|
||||
|
||||
self.buffer_probes[sink_name] = sink_pad\
|
||||
.add_buffer_probe(
|
||||
self.on_pad_buffer_probe,
|
||||
sink_name)
|
||||
|
||||
_log.info('Attached buffer probes: {0}'.format(
|
||||
self.buffer_probes))
|
||||
|
||||
break
|
||||
|
||||
seek_amount = self.position_callback(self.duration, gst)
|
||||
|
||||
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)
|
||||
|
||||
if not seek_result:
|
||||
_log.critical('Could not seek.')
|
||||
|
||||
elif self.state == self.STATE_PROCESSING_THUMBNAIL:
|
||||
_log.debug('Already processing thumbnail')
|
||||
|
||||
def on_pad_buffer_probe(self, *args):
|
||||
_log.debug('buffer probe handler: {0}'.format(args))
|
||||
gobject.idle_add(lambda: self.take_snapshot(*args))
|
||||
|
||||
def take_snapshot(self, pad, buff, name):
|
||||
if self.state == self.STATE_HALTING:
|
||||
_log.debug('Pipeline is halting, will not take snapshot')
|
||||
return False
|
||||
|
||||
_log.info('Taking snapshot! ({0})'.format(
|
||||
(pad, buff, name)))
|
||||
try:
|
||||
caps = buff.caps
|
||||
if caps is None:
|
||||
_log.error('No buffer caps present /take_snapshot')
|
||||
self.disconnect()
|
||||
|
||||
_log.debug('caps: {0}'.format(caps))
|
||||
|
||||
filters = caps[0]
|
||||
width = filters['width']
|
||||
height = filters['height']
|
||||
|
||||
im = Image.new('RGB', (width, height))
|
||||
|
||||
data = pixbuf_to_pilbuf(buff.data)
|
||||
|
||||
im.putdata(data)
|
||||
|
||||
im.save(self.dest_path)
|
||||
|
||||
_log.info('Saved snapshot!')
|
||||
|
||||
self.disconnect()
|
||||
|
||||
except gst.QueryError as exc:
|
||||
_log.error('take_snapshot - QueryError: {0}'.format(exc))
|
||||
|
||||
return False
|
||||
|
||||
def on_thumbnail_error(self):
|
||||
_log.error('Thumbnailing failed.')
|
||||
self.disconnect()
|
||||
|
||||
def disconnect(self):
|
||||
self.state = self.STATE_HALTING
|
||||
|
||||
if self.playbin is not None:
|
||||
self.playbin.set_state(gst.STATE_NULL)
|
||||
|
||||
for sink in self.playbin.sinks():
|
||||
sink_name = sink.get_name()
|
||||
sink_factory_name = sink.get_factory().get_name()
|
||||
|
||||
if sink_factory_name == 'fakesink':
|
||||
sink_pad = sink.get_pad('sink')
|
||||
sink_pad.remove_buffer_probe(self.buffer_probes[sink_name])
|
||||
del self.buffer_probes[sink_name]
|
||||
|
||||
self.playbin = None
|
||||
|
||||
if self.thumbnail_pipeline is not None:
|
||||
self.thumbnail_pipeline.set_state(gst.STATE_NULL)
|
||||
self.thumbnail_pipeline = None
|
||||
|
||||
if self.playbin_message_bus is not None:
|
||||
self.playbin_message_bus.disconnect(self.playbin_bus_watch_id)
|
||||
self.playbin_message_bus = None
|
||||
|
||||
self.halt()
|
||||
|
||||
def halt(self):
|
||||
gobject.idle_add(self.mainloop.quit)
|
||||
|
||||
def on_gobject_timeout(self):
|
||||
_log.critical('Reached gobject timeout')
|
||||
self.disconnect()
|
||||
|
||||
def get_duration(self, pipeline, attempt=1):
|
||||
if attempt == 5:
|
||||
_log.critical('Pipeline duration query retry limit reached.')
|
||||
return 0
|
||||
|
||||
try:
|
||||
return pipeline.query_duration(gst.FORMAT_TIME)[0]
|
||||
except gst.QueryError as exc:
|
||||
_log.error('Could not get duration on attempt {0}: {1}'.format(
|
||||
attempt,
|
||||
exc))
|
||||
return self.get_duration(pipeline, attempt + 1)
|
||||
|
||||
|
||||
class VideoTranscoder:
|
||||
'''
|
||||
Video transcoder
|
||||
@ -709,7 +999,7 @@ if __name__ == '__main__':
|
||||
transcoder = VideoTranscoder()
|
||||
|
||||
if options.action == 'thumbnail':
|
||||
VideoThumbnailer(*args)
|
||||
VideoThumbnailerMarkII(*args)
|
||||
elif options.action == 'video':
|
||||
def cb(data):
|
||||
print('I\'m a callback!')
|
||||
|
Loading…
x
Reference in New Issue
Block a user