From 91f5f5e791e5bc3680cac2b0103429713517f7d4 Mon Sep 17 00:00:00 2001 From: Boris Bobrov Date: Thu, 5 Jun 2014 15:42:12 +0400 Subject: [PATCH] Porting video to GStreamer 1.0 Porting includes: - thumbnailer - transcoder - metadata handling - new common discoverer for media - new tests with in-memory test video generating - handling regardless of audio availability in the file - Pythonic gst pipelines --- mediagoblin/media_types/tools.py | 21 ++ mediagoblin/media_types/video/processing.py | 70 ++-- mediagoblin/media_types/video/transcoders.py | 361 +++++++------------ mediagoblin/media_types/video/util.py | 28 +- mediagoblin/processing/__init__.py | 5 +- mediagoblin/tests/test_video.py | 114 ++++-- 6 files changed, 297 insertions(+), 302 deletions(-) diff --git a/mediagoblin/media_types/tools.py b/mediagoblin/media_types/tools.py index fe7b3772..0822f51c 100644 --- a/mediagoblin/media_types/tools.py +++ b/mediagoblin/media_types/tools.py @@ -17,6 +17,11 @@ import logging from mediagoblin import mg_globals +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst, GstPbutils, GLib +Gst.init(None) + _log = logging.getLogger(__name__) @@ -25,3 +30,19 @@ def media_type_warning(): _log.warning('Media_types have been converted to plugins. Old' ' media_types will no longer work. Please convert them' ' to plugins to continue using them.') + + +def discover(src): + ''' + Discover properties about a media file + ''' + _log.info('Discovering {0}...'.format(src)) + uri = 'file://{0}'.format(src) + discoverer = GstPbutils.Discoverer.new(60 * Gst.SECOND) + try: + info = discoverer.discover_uri(uri) + except GLib.GError as e: + _log.warning(u'Exception: {0}'.format(e)) + info = None + _log.info('Done') + return info diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index ca9a6ad9..588af282 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -47,15 +47,7 @@ class VideoTranscodingFail(BaseProcessingFail): EXCLUDED_EXTS = ["nef", "cr2"] def sniff_handler(media_file, filename): - name, ext = os.path.splitext(filename) - clean_ext = ext.lower()[1:] - - if clean_ext in EXCLUDED_EXTS: - # We don't handle this filetype, though gstreamer might think we can - return None - - transcoder = transcoders.VideoTranscoder() - data = transcoder.discover(media_file.name) + data = transcoders.discover(media_file.name) _log.info('Sniffing {0}'.format(MEDIA_TYPE)) _log.debug('Discovered: {0}'.format(data)) @@ -64,7 +56,7 @@ def sniff_handler(media_file, filename): _log.error('Could not discover {0}'.format(filename)) return None - if data['is_video'] is True: + if data.get_video_streams(): return MEDIA_TYPE return None @@ -82,51 +74,50 @@ def store_metadata(media_entry, metadata): # video is always there video_info = metadata.get_video_streams()[0] # Let's pull out the easy, not having to be converted ones first - stored_metadata = dict( - [(key, metadata[key]) - for key in [ - "videoheight", "videolength", "videowidth", - "audiorate", "audiolength", "audiochannels", "audiowidth", - "mimetype"] - if key in metadata]) - + stored_metadata = dict() + audio_info_list = metadata.get_audio_streams() + if audio_info: + audio_info = audio_info_list[0] + stored_metadata['audiochannels'] = audio_info.get_channels() + # video is always there + video_info = metadata.get_video_streams()[0] + # Let's pull out the easy, not having to be converted ones first + stored_metadata['videoheight'] = video_info.get_height() + stored_metadata['videowidth'] = video_info.get_width() + stored_metadata['videolength'] = metadata.get_duration() + stored_metadata['mimetype'] = metadata.get_tags().get_string('mimetype') # We have to convert videorate into a sequence because it's a # special type normally.. + stored_metadata['videorate'] = [video_info.get_framerate_num(), + video_info.get_framerate_denom()] - if "videorate" in metadata: - videorate = metadata["videorate"] - stored_metadata["videorate"] = [videorate.num, videorate.denom] - - # Also make a whitelist conversion of the tags. - if "tags" in metadata: - tags_metadata = metadata['tags'] - + if metadata.get_tags(): + tags_metadata = metadata.get_tags() # we don't use *all* of these, but we know these ones are # safe... + # get_string returns (success, value) tuple tags = dict( - [(key, tags_metadata[key]) + [(key, tags_metadata.get_string(key)[1]) for key in [ "application-name", "artist", "audio-codec", "bitrate", "container-format", "copyright", "encoder", "encoder-version", "license", "nominal-bitrate", "title", "video-codec"] - if key in tags_metadata]) - if 'date' in tags_metadata: - date = tags_metadata['date'] + if tags_metadata.get_string(key)[0]]) + (success, date) = tags_metadata.get_date('date') + if success: tags['date'] = "%s-%s-%s" % ( date.year, date.month, date.day) # TODO: handle timezone info; gst.get_time_zone_offset + # python's tzinfo should help - if 'datetime' in tags_metadata: - dt = tags_metadata['datetime'] + (success, dt) = tags_metadata.get_date_time('datetime') + if success: tags['datetime'] = datetime.datetime( dt.get_year(), dt.get_month(), dt.get_day(), dt.get_hour(), dt.get_minute(), dt.get_second(), dt.get_microsecond()).isoformat() - stored_metadata['tags'] = tags - # Only save this field if there's something to save if len(stored_metadata): media_entry.media_data_init( @@ -220,7 +211,10 @@ class CommonVideoProcessor(MediaProcessor): return # Extract metadata and keep a record of it - metadata = self.transcoder.discover(self.process_filename) + metadata = transcoders.discover(self.process_filename) + # metadata's stream info here is a DiscovererContainerInfo instance, + # it gets split into DiscovererAudioInfo and DiscovererVideoInfo; + # metadata itself has container-related data in tags, like video-codec store_metadata(self.entry, metadata) # Figure out whether or not we need to transcode this video or @@ -243,10 +237,8 @@ class CommonVideoProcessor(MediaProcessor): vorbis_quality=vorbis_quality, progress_callback=progress_callback, dimensions=tuple(medium_size)) - - dst_dimensions = self.transcoder.dst_data.videowidth,\ - self.transcoder.dst_data.videoheight - + video_info = self.transcoder.dst_data.get_video_streams()[0] + dst_dimensions = (video_info.get_width(), video_info.get_height()) self._keep_best() # Push transcoded video to public storage diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py index e08b897c..d53cabc6 100644 --- a/mediagoblin/media_types/video/transcoders.py +++ b/mediagoblin/media_types/video/transcoders.py @@ -19,16 +19,18 @@ from __future__ import division import os import sys import logging -import urllib import multiprocessing -import gobject +from mediagoblin.media_types.tools import discover + +#os.environ['GST_DEBUG'] = '4,python:4' old_argv = sys.argv sys.argv = [] -import pygst -pygst.require('0.10') -import gst +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst, GstPbutils +Gst.init(None) sys.argv = old_argv import struct @@ -37,12 +39,8 @@ try: except ImportError: import Image -from gst.extend import discoverer - _log = logging.getLogger(__name__) -gobject.threads_init() - CPU_COUNT = 2 try: @@ -53,57 +51,70 @@ except NotImplementedError: os.putenv('GST_DEBUG_DUMP_DOT_DIR', '/tmp') -def pixbuf_to_pilbuf(buf): - data = list() - for i in range(0, len(buf)-4, 4): - r, g, b, x = struct.unpack('BBBB', buf[i:i + 4]) - # XXX: can something be done with the 'X' part of RGBX? - data.append((r, g, b)) - return data - def capture_thumb(video_path, dest_path, width=None, height=None, percent=0.5): def pad_added(element, pad, connect_to): - caps = pad.get_caps() - name = caps[0].get_name() + '''This is a callback to dynamically add element to pipeline''' + caps = pad.query_caps(None) + name = caps.to_string() _log.debug('on_pad_added: {0}'.format(name)) if name.startswith('video') and not connect_to.is_linked(): pad.link(connect_to) - # construct pipeline: uridecodebin ! ffmpegcolorspace ! videoscale ! \ + + # construct pipeline: uridecodebin ! videoconvert ! videoscale ! \ # ! CAPS ! appsink - pipeline = gst.Pipeline() - uridecodebin = gst.element_factory_make('uridecodebin') + pipeline = Gst.Pipeline() + uridecodebin = Gst.ElementFactory.make('uridecodebin', None) uridecodebin.set_property('uri', 'file://{0}'.format(video_path)) - ffmpegcolorspace = gst.element_factory_make('ffmpegcolorspace') + videoconvert = Gst.ElementFactory.make('videoconvert', None) uridecodebin.connect('pad-added', pad_added, - ffmpegcolorspace.get_pad('sink')) - videoscale = gst.element_factory_make('videoscale') - filter = gst.element_factory_make('capsfilter', 'filter') + videoconvert.get_static_pad('sink')) + videoscale = Gst.ElementFactory.make('videoscale', None) + # create caps for video scaling - caps_struct = gst.Structure('video/x-raw-rgb') - caps_struct.set_value('pixel-aspect-ratio', gst.Fraction(1, 1)) + caps_struct = Gst.Structure.new_empty('video/x-raw') + caps_struct.set_value('pixel-aspect-ratio', Gst.Fraction(1, 1)) + caps_struct.set_value('format', 'RGB') if height: caps_struct.set_value('height', height) if width: caps_struct.set_value('width', width) - caps = gst.Caps(caps_struct) - filter.set_property('caps', caps) - appsink = gst.element_factory_make('appsink') - pipeline.add(uridecodebin, ffmpegcolorspace, videoscale, filter, appsink) - gst.element_link_many(ffmpegcolorspace, videoscale, filter, appsink) + caps = Gst.Caps.new_empty() + caps.append_structure(caps_struct) + + # sink everything to memory + appsink = Gst.ElementFactory.make('appsink', None) + appsink.set_property('caps', caps) + + # add everything to pipeline + elements = [uridecodebin, videoconvert, videoscale, appsink] + for e in elements: + pipeline.add(e) + videoconvert.link(videoscale) + videoscale.link(appsink) + # pipeline constructed, starting playing, but first some preparations - if pipeline.set_state(gst.STATE_PAUSED) == gst.STATE_CHANGE_FAILURE: - _log.warning('state change failed') - pipeline.get_state() - duration = pipeline.query_duration(gst.FORMAT_TIME, None)[0] - if duration == gst.CLOCK_TIME_NONE: + # seek to 50% of the file is required + pipeline.set_state(Gst.State.PAUSED) + # timeout of 3 seconds below was set experimentally + state = pipeline.get_state(Gst.SECOND * 3) + if state[0] != Gst.StateChangeReturn.SUCCESS: + _log.warning('state change failed, {0}'.format(state)) + return + + # get duration + (success, duration) = pipeline.query_duration(Gst.Format.TIME) + if not success: _log.warning('query_duration failed') - duration = 0 # XXX + return + seek_to = int(duration * int(percent * 100) / 100) _log.debug('Seeking to {0} of {1}'.format( - seek_to / gst.SECOND, duration / gst.SECOND)) - seek = pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_to) + float(seek_to) / Gst.SECOND, float(duration) / Gst.SECOND)) + seek = pipeline.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, seek_to) if not seek: _log.warning('seek failed') + return + # get sample, retrieve it's format and save sample = appsink.emit("pull-preroll") if not sample: @@ -112,16 +123,20 @@ def capture_thumb(video_path, dest_path, width=None, height=None, percent=0.5): caps = sample.get_caps() if not caps: _log.warning('could not get snapshot format') + return structure = caps.get_structure(0) (success, width) = structure.get_int('width') (success, height) = structure.get_int('height') buffer = sample.get_buffer() + + # get the image from the buffer and save it to disk im = Image.frombytes('RGB', (width, height), buffer.extract_dup(0, buffer.get_size())) im.save(dest_path) _log.info('thumbnail saved to {0}'.format(dest_path)) + # cleanup - pipeline.set_state(gst.STATE_NULL) + pipeline.set_state(Gst.State.NULL) class VideoTranscoder(object): @@ -130,16 +145,12 @@ class VideoTranscoder(object): Transcodes the SRC video file to a VP8 WebM video file at DST - - 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. + - Produces a WebM vp8 and vorbis video file. ''' def __init__(self): _log.info('Initializing VideoTranscoder...') self.progress_percentage = None - self.loop = gobject.MainLoop() + self.loop = GObject.MainLoop() def transcode(self, src, dst, **kwargs): ''' @@ -172,152 +183,85 @@ class VideoTranscoder(object): if not type(self.destination_dimensions) == tuple: raise Exception('dimensions must be tuple: (width, height)') - self._setup() - self._run() - - # XXX: This could be a static method. - def discover(self, src): - ''' - Discover properties about a media file - ''' - _log.info('Discovering {0}'.format(src)) - - self.source_path = src - self._setup_discover(discovered_callback=self.__on_discovered) - - self.discoverer.discover() - - self.loop.run() - if hasattr(self, '_discovered_data'): - return self._discovered_data.__dict__ - else: - return None - - def __on_discovered(self, data, is_media): - _log.debug('Discovered: {0}'.format(data)) - if not is_media: - self.__stop() - raise Exception('Could not discover {0}'.format(self.source_path)) - - self._discovered_data = data - - self.__stop_mainloop() - - def _setup(self): - self._setup_discover() self._setup_pipeline() - - def _run(self): - _log.info('Discovering...') - self.discoverer.discover() - _log.info('Done') - - _log.debug('Initializing MainLoop()') - self.loop.run() - - def _setup_discover(self, **kw): - _log.debug('Setting up discoverer') - self.discoverer = discoverer.Discoverer(self.source_path) - - # Connect self.__discovered to the 'discovered' event - self.discoverer.connect( - 'discovered', - kw.get('discovered_callback', 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.__dict__)) - - self.data = data - - # Launch things that should be done after discovery + self.data = discover(self.source_path) self._link_elements() self.__setup_videoscale_capsfilter() - - # Tell the transcoding pipeline to start running - self.pipeline.set_state(gst.STATE_PLAYING) + self.pipeline.set_state(Gst.State.PLAYING) _log.info('Transcoding...') + _log.debug('Initializing MainLoop()') + self.loop.run() + def _setup_pipeline(self): _log.debug('Setting up transcoding pipeline') # Create the pipeline bin. - self.pipeline = gst.Pipeline('VideoTranscoderPipeline') + self.pipeline = Gst.Pipeline.new('VideoTranscoderPipeline') # Create all GStreamer elements, starting with # filesrc & decoder - self.filesrc = gst.element_factory_make('filesrc', 'filesrc') + self.filesrc = Gst.ElementFactory.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.decoder = Gst.ElementFactory.make('decodebin', 'decoder') + self.decoder.connect('pad-added', self._on_dynamic_pad) self.pipeline.add(self.decoder) # Video elements - self.videoqueue = gst.element_factory_make('queue', 'videoqueue') + self.videoqueue = Gst.ElementFactory.make('queue', 'videoqueue') self.pipeline.add(self.videoqueue) - self.videorate = gst.element_factory_make('videorate', 'videorate') + self.videorate = Gst.ElementFactory.make('videorate', 'videorate') self.pipeline.add(self.videorate) - self.ffmpegcolorspace = gst.element_factory_make( - 'ffmpegcolorspace', 'ffmpegcolorspace') - self.pipeline.add(self.ffmpegcolorspace) + self.videoconvert = Gst.ElementFactory.make('videoconvert', + 'videoconvert') + self.pipeline.add(self.videoconvert) - self.videoscale = gst.element_factory_make('ffvideoscale', 'videoscale') - #self.videoscale.set_property('method', 2) # I'm not sure this works - #self.videoscale.set_property('add-borders', 0) + self.videoscale = Gst.ElementFactory.make('videoscale', 'videoscale') self.pipeline.add(self.videoscale) - self.capsfilter = gst.element_factory_make('capsfilter', 'capsfilter') + self.capsfilter = Gst.ElementFactory.make('capsfilter', 'capsfilter') self.pipeline.add(self.capsfilter) - self.vp8enc = gst.element_factory_make('vp8enc', 'vp8enc') - self.vp8enc.set_property('quality', self.vp8_quality) + self.vp8enc = Gst.ElementFactory.make('vp8enc', 'vp8enc') self.vp8enc.set_property('threads', self.vp8_threads) - self.vp8enc.set_property('max-latency', 25) self.pipeline.add(self.vp8enc) # Audio elements - self.audioqueue = gst.element_factory_make('queue', 'audioqueue') + self.audioqueue = Gst.ElementFactory.make('queue', 'audioqueue') self.pipeline.add(self.audioqueue) - self.audiorate = gst.element_factory_make('audiorate', 'audiorate') + self.audiorate = Gst.ElementFactory.make('audiorate', 'audiorate') self.audiorate.set_property('tolerance', 80000000) self.pipeline.add(self.audiorate) - self.audioconvert = gst.element_factory_make('audioconvert', 'audioconvert') + self.audioconvert = Gst.ElementFactory.make('audioconvert', 'audioconvert') self.pipeline.add(self.audioconvert) - self.audiocapsfilter = gst.element_factory_make('capsfilter', - 'audiocapsfilter') - audiocaps = ['audio/x-raw-float'] - self.audiocapsfilter.set_property( - 'caps', - gst.caps_from_string( - ','.join(audiocaps))) + self.audiocapsfilter = Gst.ElementFactory.make('capsfilter', + 'audiocapsfilter') + audiocaps = Gst.Caps.new_empty() + audiocaps_struct = Gst.Structure.new_empty('audio/x-raw') + audiocaps.append_structure(audiocaps_struct) + self.audiocapsfilter.set_property('caps', audiocaps) self.pipeline.add(self.audiocapsfilter) - self.vorbisenc = gst.element_factory_make('vorbisenc', 'vorbisenc') + self.vorbisenc = Gst.ElementFactory.make('vorbisenc', 'vorbisenc') self.vorbisenc.set_property('quality', self.vorbis_quality) self.pipeline.add(self.vorbisenc) # WebMmux & filesink - self.webmmux = gst.element_factory_make('webmmux', 'webmmux') + self.webmmux = Gst.ElementFactory.make('webmmux', 'webmmux') self.pipeline.add(self.webmmux) - self.filesink = gst.element_factory_make('filesink', 'filesink') + self.filesink = Gst.ElementFactory.make('filesink', 'filesink') self.filesink.set_property('location', self.destination_path) self.pipeline.add(self.filesink) # Progressreport - self.progressreport = gst.element_factory_make( + self.progressreport = Gst.ElementFactory.make( 'progressreport', 'progressreport') # Update every second self.progressreport.set_property('update-freq', 1) @@ -336,48 +280,41 @@ class VideoTranscoder(object): # '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 row to webmmux - gst.element_link_many( - self.videoqueue, - self.videorate, - self.ffmpegcolorspace, - self.videoscale, - self.capsfilter, - self.vp8enc, - self.webmmux) + # link the rest + self.videoqueue.link(self.videorate) + self.videorate.link(self.videoconvert) + self.videoconvert.link(self.videoscale) + self.videoscale.link(self.capsfilter) + self.capsfilter.link(self.vp8enc) + self.vp8enc.link(self.webmmux) if self.data.is_audio: - # Link all the audio elements in a row to webmux - gst.element_link_many( - self.audioqueue, - self.audiorate, - self.audioconvert, - self.audiocapsfilter, - self.vorbisenc, - self.webmmux) - - gst.element_link_many( - self.webmmux, - self.progressreport, - self.filesink) + # Link all the audio elements in a row to webmmux + self.audioqueue.link(self.audiorate) + self.audiorate.link(self.audioconvert) + self.audioconvert.link(self.audiocapsfilter) + self.audiocapsfilter.link(self.vorbisenc) + self.vorbisenc.link(self.webmmux) + 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): + def _on_dynamic_pad(self, dbin, pad): ''' - Callback called when ``decodebin2`` has a pad that we can connect to + Callback called when ``decodebin`` 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 self.ffmpegcolorspace.get_pad_template('sink')\ - .get_caps().intersect(pad.get_caps()).is_empty(): + if (self.videorate.get_static_pad('sink').get_pad_template() + .get_caps().intersect(pad.query_caps()).is_empty()): # It is NOT a video src pad. - pad.link(self.audioqueue.get_pad('sink')) + pad.link(self.audioqueue.get_static_pad('sink')) else: # It IS a video src pad. - pad.link(self.videoqueue.get_pad('sink')) + _log.debug('linking video to the pad dynamically') + pad.link(self.videoqueue.get_static_pad('sink')) def _setup_bus(self): self.bus = self.pipeline.get_bus() @@ -388,73 +325,53 @@ class VideoTranscoder(object): ''' Sets up the output format (width, height) for the video ''' - caps = ['video/x-raw-yuv', 'pixel-aspect-ratio=1/1', 'framerate=30/1'] - - if self.data.videoheight > self.data.videowidth: - # Whoa! We have ourselves a portrait video! - caps.append('height={0}'.format( - self.destination_dimensions[1])) + caps_struct = Gst.Structure.new_empty('video/x-raw') + caps_struct.set_value('pixel-aspect-ratio', Gst.Fraction(1, 1)) + caps_struct.set_value('framerate', Gst.Fraction(30, 1)) + video_info = self.data.get_video_streams()[0] + if video_info.get_height() > video_info.get_width(): + # portrait + caps_struct.set_value('height', 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( - ','.join(caps))) + # landscape + caps_struct.set_value('width', self.destination_dimensions[0]) + caps = Gst.Caps.new_empty() + caps.append_structure(caps_struct) + self.capsfilter.set_property('caps', caps) def _on_message(self, bus, message): _log.debug((bus, message, message.type)) - - t = message.type - - if message.type == gst.MESSAGE_EOS: - self._discover_dst_and_stop() + if message.type == Gst.MessageType.EOS: + self.dst_data = discover(self.destination_path) + self.__stop() _log.info('Done') - - elif message.type == gst.MESSAGE_ELEMENT: - if message.structure.get_name() == 'progress': - data = dict(message.structure) + elif message.type == Gst.MessageType.ELEMENT: + if message.has_name('progress'): + structure = message.get_structure() # Update progress state if it has changed - if self.progress_percentage != data.get('percent'): - self.progress_percentage = data.get('percent') + (success, percent) = structure.get_int('percent') + if self.progress_percentage != percent and success: + self.progress_percentage = percent if self._progress_callback: - self._progress_callback(data.get('percent')) - - _log.info('{percent}% done...'.format( - percent=data.get('percent'))) - _log.debug(data) - - elif t == gst.MESSAGE_ERROR: - _log.error((bus, message)) + self._progress_callback(percent) + _log.info('{percent}% done...'.format(percent=percent)) + elif message.type == Gst.MessageType.ERROR: + _log.error('Got error: {0}'.format(message.parse_error())) self.__stop() - def _discover_dst_and_stop(self): - self.dst_discoverer = discoverer.Discoverer(self.destination_path) - - self.dst_discoverer.connect('discovered', self.__dst_discovered) - - self.dst_discoverer.discover() - - def __dst_discovered(self, data, is_media): - self.dst_data = data - - self.__stop() - def __stop(self): _log.debug(self.loop) if hasattr(self, 'pipeline'): # Stop executing the pipeline - self.pipeline.set_state(gst.STATE_NULL) + self.pipeline.set_state(Gst.State.NULL) # This kills the loop, mercifully - gobject.idle_add(self.__stop_mainloop) + GObject.idle_add(self.__stop_mainloop) def __stop_mainloop(self): ''' - Wrapper for gobject.MainLoop.quit() + Wrapper for GObject.MainLoop.quit() This wrapper makes us able to see if self.loop.quit has been called ''' diff --git a/mediagoblin/media_types/video/util.py b/mediagoblin/media_types/video/util.py index 29b7f410..4dc395b4 100644 --- a/mediagoblin/media_types/video/util.py +++ b/mediagoblin/media_types/video/util.py @@ -33,27 +33,33 @@ def skip_transcode(metadata, size): medium_config = mgg.global_config['media:medium'] _log.debug('skip_transcode config: {0}'.format(config)) - - if config['mime_types'] and metadata.get('mimetype'): - if not metadata['mimetype'] in config['mime_types']: + tags = metadata.get_tags() + if config['mime_types'] and tags.get_string('mimetype'): + if not tags.get_string('mimetype') in config['mime_types']: return False - if config['container_formats'] and metadata['tags'].get('container-format'): - if not metadata['tags']['container-format'] in config['container_formats']: + if config['container_formats'] and tags.get_string('container-format'): + if not (metadata.get_tags().get_string('container-format') in + config['container_formats']): return False - if config['video_codecs'] and metadata['tags'].get('video-codec'): - if not metadata['tags']['video-codec'] in config['video_codecs']: + if (config['video_codecs'] and + metadata.get_tags().get_string('video-codec')): + if not (metadata.get_tags().get_string('video-codec') in + config['video_codecs']): return False - if config['audio_codecs'] and metadata['tags'].get('audio-codec'): - if not metadata['tags']['audio-codec'] in config['audio_codecs']: + if (config['audio_codecs'] and + metadata.get_tags().get_string('audio-codec')): + if not (metadata.get_tags().get_string('audio-codec') in + config['audio_codecs']): return False + video_info = metadata.get_video_streams()[0] if config['dimensions_match']: - if not metadata['videoheight'] <= size[1]: + if not video_info.get_height() <= size[1]: return False - if not metadata['videowidth'] <= size[0]: + if not video_info.get_width() <= size[0]: return False return True diff --git a/mediagoblin/processing/__init__.py b/mediagoblin/processing/__init__.py index 5a88ddea..b7e36027 100644 --- a/mediagoblin/processing/__init__.py +++ b/mediagoblin/processing/__init__.py @@ -378,12 +378,11 @@ def store_public(entry, keyname, local_file, target_name=None, entry.media_files[keyname], target_filepath) if delete_if_exists: mgg.public_store.delete_file(entry.media_files[keyname]) - try: mgg.public_store.copy_local_to_storage(local_file, target_filepath) - except: + except Exception as e: + _log.error(u'Exception happened: {0}'.format(e)) raise PublicStoreFail(keyname=keyname) - # raise an error if the file failed to copy if not mgg.public_store.file_exists(target_filepath): raise PublicStoreFail(keyname=keyname) diff --git a/mediagoblin/tests/test_video.py b/mediagoblin/tests/test_video.py index 0fe58f60..03298b67 100644 --- a/mediagoblin/tests/test_video.py +++ b/mediagoblin/tests/test_video.py @@ -15,57 +15,117 @@ # along with this program. If not, see . import tempfile -import shutil import os -import pytest from contextlib import contextmanager -import logging import imghdr +#os.environ['GST_DEBUG'] = '4,python:4' + #TODO: this should be skipped if video plugin is not enabled -import pygst -pygst.require('0.10') -import gst +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst +Gst.init(None) -from mediagoblin.media_types.video.transcoders import capture_thumb +from mediagoblin.media_types.video.transcoders import (capture_thumb, + VideoTranscoder) +from mediagoblin.media_types.tools import discover @contextmanager -def create_data(suffix): +def create_data(suffix=None, make_audio=False): video = tempfile.NamedTemporaryFile() - src = gst.element_factory_make('videotestsrc') - src.set_property('num-buffers', 50) - enc = gst.element_factory_make('theoraenc') - mux = gst.element_factory_make('oggmux') - dst = gst.element_factory_make('filesink') + src = Gst.ElementFactory.make('videotestsrc', None) + src.set_property('num-buffers', 10) + videorate = Gst.ElementFactory.make('videorate', None) + enc = Gst.ElementFactory.make('theoraenc', None) + mux = Gst.ElementFactory.make('oggmux', None) + dst = Gst.ElementFactory.make('filesink', None) dst.set_property('location', video.name) - pipeline = gst.Pipeline() - pipeline.add(src, enc, mux, dst) - gst.element_link_many(src, enc, mux, dst) - pipeline.set_state(gst.STATE_PLAYING) - # wait for finish + pipeline = Gst.Pipeline() + pipeline.add(src) + pipeline.add(videorate) + pipeline.add(enc) + pipeline.add(mux) + pipeline.add(dst) + src.link(videorate) + videorate.link(enc) + enc.link(mux) + mux.link(dst) + if make_audio: + audio_src = Gst.ElementFactory.make('audiotestsrc', None) + audio_src.set_property('num-buffers', 10) + audiorate = Gst.ElementFactory.make('audiorate', None) + audio_enc = Gst.ElementFactory.make('vorbisenc', None) + pipeline.add(audio_src) + pipeline.add(audio_enc) + pipeline.add(audiorate) + audio_src.link(audiorate) + audiorate.link(audio_enc) + audio_enc.link(mux) + pipeline.set_state(Gst.State.PLAYING) + state = pipeline.get_state(3 * Gst.SECOND) + assert state[0] == Gst.StateChangeReturn.SUCCESS bus = pipeline.get_bus() - message = bus.timed_pop_filtered(gst.CLOCK_TIME_NONE, - gst.MESSAGE_ERROR | gst.MESSAGE_EOS) - thumb = tempfile.NamedTemporaryFile(suffix=suffix) - pipeline.set_state(gst.STATE_NULL) - yield (video.name, thumb.name) + message = bus.timed_pop_filtered( + 3 * Gst.SECOND, + Gst.MessageType.ERROR | Gst.MessageType.EOS) + pipeline.set_state(Gst.State.NULL) + if suffix: + result = tempfile.NamedTemporaryFile(suffix=suffix) + else: + result = tempfile.NamedTemporaryFile() + yield (video.name, result.name) #TODO: this should be skipped if video plugin is not enabled def test_thumbnails(): ''' Test thumbnails generation. - 1. Create a video from gst's videotestsrc - 3. Capture thumbnail - 4. Remove it + 1. Create a video (+audio) from gst's videotestsrc + 2. Capture thumbnail + 3. Everything should get removed because of temp files usage ''' #data create_data() as (video_name, thumbnail_name): test_formats = [('.png', 'png'), ('.jpg', 'jpeg'), ('.gif', 'gif')] for suffix, format in test_formats: with create_data(suffix) as (video_name, thumbnail_name): capture_thumb(video_name, thumbnail_name, width=40) - # check if png + # check result file format assert imghdr.what(thumbnail_name) == format # TODO: check height and width # FIXME: it doesn't work with small width, say, 10px. This should be # fixed somehow + suffix, format = test_formats[0] + with create_data(suffix, True) as (video_name, thumbnail_name): + capture_thumb(video_name, thumbnail_name, width=40) + assert imghdr.what(thumbnail_name) == format + with create_data(suffix, True) as (video_name, thumbnail_name): + capture_thumb(video_name, thumbnail_name, width=10) # smaller width + assert imghdr.what(thumbnail_name) == format + with create_data(suffix, True) as (video_name, thumbnail_name): + capture_thumb(video_name, thumbnail_name, width=100) # bigger width + assert imghdr.what(thumbnail_name) == format + + +def test_transcoder(): + # test without audio + with create_data() as (video_name, result_name): + transcoder = VideoTranscoder() + transcoder.transcode( + video_name, result_name, + vp8_quality=8, + vp8_threads=0, # autodetect + vorbis_quality=0.3, + dimensions=(640, 640)) + assert len(discover(result_name).get_video_streams()) == 1 + # test with audio + with create_data(make_audio=True) as (video_name, result_name): + transcoder = VideoTranscoder() + transcoder.transcode( + video_name, result_name, + vp8_quality=8, + vp8_threads=0, # autodetect + vorbis_quality=0.3, + dimensions=(640, 640)) + assert len(discover(result_name).get_video_streams()) == 1 + assert len(discover(result_name).get_audio_streams()) == 1 -- 2.25.1